[
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL Advanced\"\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n  schedule:\n    - cron: '18 0 * * 5'\n\njobs:\n  analyze:\n    name: Analyze (${{ matrix.language }})\n    # Runner size impacts CodeQL analysis time. To learn more, please see:\n    #   - https://gh.io/recommended-hardware-resources-for-running-codeql\n    #   - https://gh.io/supported-runners-and-hardware-resources\n    #   - https://gh.io/using-larger-runners (GitHub.com only)\n    # Consider using larger runners or machines with greater resources for possible analysis time improvements.\n    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}\n    permissions:\n      # required for all workflows\n      security-events: write\n\n      # required to fetch internal or private CodeQL packs\n      packages: read\n\n      # only required for workflows in private repositories\n      actions: read\n      contents: read\n\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - language: actions\n          build-mode: none\n        - language: csharp\n          build-mode: none\n        - language: javascript-typescript\n          build-mode: none\n        # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'\n        # Use `c-cpp` to analyze code written in C, C++ or both\n        # Use 'java-kotlin' to analyze code written in Java, Kotlin or both\n        # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both\n        # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,\n        # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.\n        # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how\n        # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n\n    # Add any setup steps before running the `github/codeql-action/init` action.\n    # This includes steps like installing compilers or runtimes (`actions/setup-node`\n    # or others). This is typically only required for manual builds.\n    # - name: Setup runtime (example)\n    #   uses: actions/setup-example@v1\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v3\n      with:\n        languages: ${{ matrix.language }}\n        build-mode: ${{ matrix.build-mode }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n        # queries: security-extended,security-and-quality\n\n    # If the analyze step fails for one of the languages you are analyzing with\n    # \"We were unable to automatically build your code\", modify the matrix above\n    # to set the build mode to \"manual\" for that language. Then modify this step\n    # to build your code.\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n    - if: matrix.build-mode == 'manual'\n      shell: bash\n      run: |\n        echo 'If you are using a \"manual\" build mode for one or more of the' \\\n          'languages you are analyzing, replace this with the commands to build' \\\n          'your code, for example:'\n        echo '  make bootstrap'\n        echo '  make release'\n        exit 1\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v3\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".gitignore",
    "content": "packages\n/.vs/config\n/.vs\n/*.user\n*.user\n.idea\n.idea/.idea.PopForums/.idea\n.idea/.idea.PopForums/riderModule.iml\n.DS_Store\n\nbin\nobj\npackages\n*.pubxml\nnode_modules\n\nsrc/PopForums.AzureKit.Functions/Properties/ServiceDependencies\n\nsrc/PopForums.Mvc/wwwroot/lib/*\nsrc/PopForums.Mvc/wwwroot/PopForums.js\nsrc/PopForums.Mvc/package-lock.json\nsrc/PopForums.Mvc/node_modules\n\nsrc/PopForums.Web/Areas/Forums\n/src/PopForums.Web/appsettings.development.json\n/src/PopForums.AzureKit.Functions/local.settings.dev.json\n/.claude\n.claude/settings.local.json\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nPOP Forums is an ASP.NET Core forum and Q&A application targeting .NET 10. It uses SignalR for real-time updates, TypeScript for front-end components, and SQL Server as the primary data store.\n\n## Solution Structure\n\n| Project | Purpose |\n|---|---|\n| `PopForums` | Core business logic, service interfaces, repository interfaces, models |\n| `PopForums.Sql` | SQL Server data access implementations, caching layer, migrations scripts |\n| `PopForums.Mvc` | ASP.NET MVC area (`/Forums`), controllers, views, TypeScript client, CSS |\n| `PopForums.Web` | Host app template — references above projects, contains `Program.cs` |\n| `PopForums.AzureKit` | Azure-specific implementations: Redis cache, Azure Search, Blob Storage, queues |\n| `PopForums.AzureKit.Functions` | Azure Functions implementations for background jobs |\n| `PopForums.ElasticKit` | ElasticSearch search implementation |\n| `PopForums.Test` | xUnit tests using NSubstitute, covers services and some MVC code |\n\n## Build Commands\n\n### .NET\n```bash\n# Build entire solution\ndotnet build PopForums.sln\n\n# Run all tests\ndotnet test src/PopForums.Test/PopForums.Test.csproj\n\n# Run a single test class\ndotnet test src/PopForums.Test/PopForums.Test.csproj --filter \"FullyQualifiedName~PostMasterServiceTests\"\n\n# Run a single test method\ndotnet test src/PopForums.Test/PopForums.Test.csproj --filter \"FullyQualifiedName~PostMasterServiceTests.SomeMethodName\"\n```\n\n### Front-end asset setup (run once from `src/PopForums.Mvc/`)\n```bash\nnpm install\nnpx gulp copies   # copy node_modules assets (Bootstrap, SignalR, TinyMCE, Vue, etc.) to wwwroot/lib\nnpx gulp css      # minify CSS\n```\n\nTypeScript compilation is handled automatically by `Microsoft.TypeScript.MSBuild` as part of the .NET build — no manual `tsc` or `gulp ts` needed. The Mvc project's static assets (JS, CSS, fonts) are embedded into the NuGet package and served to the host app via `StaticWebAssetBasePath=/PopForums`. The app itself is run from `PopForums.Web`, not `PopForums.Mvc`.\n\n## Running Locally\n\nThe `PopForums.Web` project is the host application. Key setup steps:\n\n1. Set the connection string in `appsettings.json` under `PopForums:Database:ConnectionString` (default looks for a local SQL Server DB named `popforums21`)\n2. First run: navigate to `/Forums/Setup` to initialize the database and admin account (don't run the SQL script manually before this)\n3. Background jobs: by default `Program.cs` uses `AddPopForumsAzureFunctionsAndQueues()`. For local development without Azure, switch to `AddPopForumsBackgroundJobs()` (in-process)\n\n### Docker services for local dev\n```bash\n# SQL Server (ARM: azure-sql-edge; x86: mssql/server:2022-latest)\ndocker run --cap-add SYS_PTRACE -e 'ACCEPT_EULA=1' -e 'MSSQL_SA_PASSWORD=P@ssw0rd' -p 1433:1433 --name sqledge -d mcr.microsoft.com/azure-sql-edge\n\n# Azurite (storage + queues)\ndocker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite\n\n# Redis (distributed cache / SignalR backplane)\ndocker run --name some-redis -p 6379:6379 -d redis\n\n# ElasticSearch\ndocker run --name es-9 -p 9200:9200 -e discovery.type=single-node -it docker.elastic.co/elasticsearch/elasticsearch:9.3.0\n```\n\n## Architecture Patterns\n\n### Layered dependency direction\n`PopForums.Mvc` → `PopForums` (interfaces) ← `PopForums.Sql` (implementations)\n\n- `PopForums` defines all repository interfaces (`IForumRepository`, `IPostRepository`, etc.) in `Repositories/` and service classes in `Services/`\n- `PopForums.Sql` implements those repository interfaces with SQL Server + Dapper-style access\n- Services depend on repository interfaces; they never touch SQL directly\n- `PopForums.AzureKit` and `PopForums.ElasticKit` swap in alternative implementations via DI extension methods\n\n### DI registration pattern\nEach library exposes extension methods on `IServiceCollection`:\n- `services.AddPopForumsSql()` — registers SQL repos and in-memory cache\n- `services.AddPopForumsMvc()` — registers MVC services, auth, and base forum services\n- `services.AddPopForumsRedisCache()` — overrides cache with Redis two-level cache\n- `services.AddPopForumsElasticSearch()` / `services.AddPopForumsAzureSearch()` — override search\n- `services.AddPopForumsAzureFunctionsAndQueues()` — routes background work to Azure queues\n- `services.AddPopForumsBackgroundJobs()` — runs background jobs in-process (local dev)\n\n### MVC Area\nAll forum routes are under the `Forums` area. Controllers live in `src/PopForums.Mvc/Areas/Forums/Controllers/`. The area is mapped via `app.AddPopForumsEndpoints()`.\n\n### Background jobs\nBackground tasks (email, search indexing, award calculation, session cleanup, etc.) are implemented as `BackgroundService` derivatives. In production, these run as Azure Functions via `PopForums.AzureKit.Functions`. In-process mode is available for single-node or local use.\n\n### Front-end\n- No SPA framework for the main forum UI — raw TypeScript components in `Client/Components/`\n- Components extend `ElementBase.ts` and use a simple state engine in `State/`\n- SignalR connects on page load; components react to hub messages for real-time updates\n- Vue.js + Vue Router are used **only** for the admin interface\n- Localization on the client side uses a JSON payload from the server; see `FormattedTime.ts` for an example\n\n### Testing\n- Tests are in `PopForums.Test`, mirroring the folder structure of the projects under test\n- Mocking via NSubstitute; test framework is xUnit\n- Tests focus on service layer; controller and repository coverage is minimal\n\n## Database\n\n- Initial schema: `src/PopForums.Sql/PopForums.sql`\n- Migration scripts follow the pattern `PopForumsXXtoYY.sql` in `src/PopForums.Sql/`\n- From v21.x to v22.x, run `PopForums21to22.sql`\n- Statistics (post counts, etc.) are precomputed at write time rather than aggregated at query time\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jeff@popw.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "POP Forums\r\nCopyright (c)1999-2023, POP World Media, LLC and Jeffrey M. Putz\r\nhttps://popw.com/\r\nhttps://jeffputz.com/\r\n\r\nTRANSLATIONS PROVIDED BY:\r\n\r\nSpanish: Mauricio Atanache\r\nDutch: Steven van Deursen\r\nUkrainian: Nazar Harasym\r\nGerman: Manfred Theis\r\nTaiwanese Mandarin: Cheng Liu\r\n\r\nMIT LICENSE:\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a copy\r\nof this software and associated documentation files (the \"Software\"), to deal\r\nin the Software without restriction, including without limitation the rights\r\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\ncopies of the Software, and to permit persons to whom the Software is\r\nfurnished to do so, subject to the following conditions:\r\n\r\nThe above copyright notice and this permission notice shall be included in all\r\ncopies or substantial portions of the Software.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r\nSOFTWARE.\r\n\r\nOTHER LICENSES:\r\nNSubstitute: BSD 3-Clause\r\n.NET Core, ASP.NET Core, Bootstrap, popper.js, MailKit, TinyMCE: MIT\r\nSignalR, xunit: Apache 2.0\r\nImageSharp: Apache 2.0 as long as you meet the open source and/or revenue stipulations \r\nindicated in their license: https://github.com/SixLabors/ImageSharp/blob/main/LICENSE\r\n"
  },
  {
    "path": "NuGet.config",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n\t<activePackageSource>\n\t\t<add key=\"All\" value=\"(Aggregate source)\" />\n\t</activePackageSource>\n\t<packageSources>\n\t\t<add key=\"nuget.org\" value=\"https://api.nuget.org/v3/index.json\" />\n\t\t<add key=\"pi-prerelease\" value=\"https://www.myget.org/F/popidentity/api/v3/index.json\" />\n\t</packageSources>\n\t<disabledPackageSources>\n\t\t<add key=\"prerelease\" value=\"true\" />\n\t</disabledPackageSources>\n\t<packageSourceCredentials>\n\t</packageSourceCredentials>\n</configuration>"
  },
  {
    "path": "PopForums.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.28803.156\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Solution Items\", \"Solution Items\", \"{044AC2DE-7D9D-4C1A-8C83-38AF448B5A61}\"\n\tProjectSection(SolutionItems) = preProject\n\t\t.gitignore = .gitignore\n\t\tCODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md\n\t\tLICENSE.txt = LICENSE.txt\n\t\tNuGet.config = NuGet.config\n\t\tREADME.md = README.md\n\t\tCLAUDE.md = CLAUDE.md\n\tEndProjectSection\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PopForums.Web\", \"src\\PopForums.Web\\PopForums.Web.csproj\", \"{78900BC9-3B9B-42C1-9112-8412A7AACFB7}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PopForums\", \"src\\PopForums\\PopForums.csproj\", \"{1787FE89-F023-482E-ABBA-996554DD332D}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PopForums.Sql\", \"src\\PopForums.Sql\\PopForums.Sql.csproj\", \"{229CDD35-A4A5-44F2-8937-61AB286D6A77}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PopForums.Test\", \"src\\PopForums.Test\\PopForums.Test.csproj\", \"{AB4006F4-F457-490B-B9FC-8DE9CDA67127}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PopForums.AzureKit\", \"src\\PopForums.AzureKit\\PopForums.AzureKit.csproj\", \"{C3E4460C-E785-4FF4-AFAF-07AF2EA8812D}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PopForums.Mvc\", \"src\\PopForums.Mvc\\PopForums.Mvc.csproj\", \"{0AE33580-28F6-41CB-B757-56E02394F645}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PopForums.ElasticKit\", \"src\\PopForums.ElasticKit\\PopForums.ElasticKit.csproj\", \"{20986460-15F5-4C08-BAE2-226F12B1CB23}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PopForums.AzureKit.Functions\", \"src\\PopForums.AzureKit.Functions\\PopForums.AzureKit.Functions.csproj\", \"{D50E69CC-5070-41DD-A881-20D29B1913EE}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tCI|Any CPU = CI|Any CPU\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{78900BC9-3B9B-42C1-9112-8412A7AACFB7}.CI|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{78900BC9-3B9B-42C1-9112-8412A7AACFB7}.CI|Any CPU.Build.0 = Release|Any CPU\n\t\t{78900BC9-3B9B-42C1-9112-8412A7AACFB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{78900BC9-3B9B-42C1-9112-8412A7AACFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{78900BC9-3B9B-42C1-9112-8412A7AACFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{78900BC9-3B9B-42C1-9112-8412A7AACFB7}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{1787FE89-F023-482E-ABBA-996554DD332D}.CI|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1787FE89-F023-482E-ABBA-996554DD332D}.CI|Any CPU.Build.0 = Release|Any CPU\n\t\t{1787FE89-F023-482E-ABBA-996554DD332D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{1787FE89-F023-482E-ABBA-996554DD332D}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{1787FE89-F023-482E-ABBA-996554DD332D}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{1787FE89-F023-482E-ABBA-996554DD332D}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{229CDD35-A4A5-44F2-8937-61AB286D6A77}.CI|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{229CDD35-A4A5-44F2-8937-61AB286D6A77}.CI|Any CPU.Build.0 = Release|Any CPU\n\t\t{229CDD35-A4A5-44F2-8937-61AB286D6A77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{229CDD35-A4A5-44F2-8937-61AB286D6A77}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{229CDD35-A4A5-44F2-8937-61AB286D6A77}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{229CDD35-A4A5-44F2-8937-61AB286D6A77}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{AB4006F4-F457-490B-B9FC-8DE9CDA67127}.CI|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AB4006F4-F457-490B-B9FC-8DE9CDA67127}.CI|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AB4006F4-F457-490B-B9FC-8DE9CDA67127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AB4006F4-F457-490B-B9FC-8DE9CDA67127}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AB4006F4-F457-490B-B9FC-8DE9CDA67127}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AB4006F4-F457-490B-B9FC-8DE9CDA67127}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{C3E4460C-E785-4FF4-AFAF-07AF2EA8812D}.CI|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{C3E4460C-E785-4FF4-AFAF-07AF2EA8812D}.CI|Any CPU.Build.0 = Debug|Any CPU\n\t\t{C3E4460C-E785-4FF4-AFAF-07AF2EA8812D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{C3E4460C-E785-4FF4-AFAF-07AF2EA8812D}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{C3E4460C-E785-4FF4-AFAF-07AF2EA8812D}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{C3E4460C-E785-4FF4-AFAF-07AF2EA8812D}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{0AE33580-28F6-41CB-B757-56E02394F645}.CI|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{0AE33580-28F6-41CB-B757-56E02394F645}.CI|Any CPU.Build.0 = Debug|Any CPU\n\t\t{0AE33580-28F6-41CB-B757-56E02394F645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{0AE33580-28F6-41CB-B757-56E02394F645}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{0AE33580-28F6-41CB-B757-56E02394F645}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{0AE33580-28F6-41CB-B757-56E02394F645}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{20986460-15F5-4C08-BAE2-226F12B1CB23}.CI|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{20986460-15F5-4C08-BAE2-226F12B1CB23}.CI|Any CPU.Build.0 = Debug|Any CPU\n\t\t{20986460-15F5-4C08-BAE2-226F12B1CB23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{20986460-15F5-4C08-BAE2-226F12B1CB23}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{20986460-15F5-4C08-BAE2-226F12B1CB23}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{20986460-15F5-4C08-BAE2-226F12B1CB23}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{D50E69CC-5070-41DD-A881-20D29B1913EE}.CI|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{D50E69CC-5070-41DD-A881-20D29B1913EE}.CI|Any CPU.Build.0 = Debug|Any CPU\n\t\t{D50E69CC-5070-41DD-A881-20D29B1913EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{D50E69CC-5070-41DD-A881-20D29B1913EE}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{D50E69CC-5070-41DD-A881-20D29B1913EE}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{D50E69CC-5070-41DD-A881-20D29B1913EE}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {DC971413-E75E-4D0A-8189-B71EE5DBAED1}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "PopForums.sln.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue\">IP</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PM/@EntryIndexedValue\">PM</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PMID/@EntryIndexedValue\">PMID</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QA/@EntryIndexedValue\">QA</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue\">UI</s:String>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Sitemap/@EntryIndexedValue\">True</s:Boolean></wpf:ResourceDictionary>"
  },
  {
    "path": "README.md",
    "content": "![POP Forums logo](https://avatars2.githubusercontent.com/u/8217691?s=200&v=4)\n\nPOP Forums\n=========\n\nA forum and Q&A application with real-time updating, image uploading and private message chat in multiple languages.\n\nThe main branch is now the work-in-progress for future versions running on .NET 10+. The v22.x branch is v22.x, running on .NET 10. If you're looking for the version that works on .NET Framework 4.5+ with MVC 5, check out v13.0.2.\n\nRoadmap:\nv22 is another iterative release, with the biggest feature being an ignore list. I've observed fast page rendering, average 19ms on Azure App Service P0v3 and SQL elastic pool at 50 eDTUs and 900k posts. Future versions will consider issues in the backlog.\n\nFor the latest information and documentation, and how to get started, check the pages (also in markdown in `/docs` of source):  \nhttps://popworldmedia.github.io/POPForums/\n\nCI build of main runs here:  \nhttps://popforumsdev.azurewebsites.net/Forums\n\n[![Build status](https://dev.azure.com/popw/POP%20Forums/_apis/build/status/popforumsdev)](https://dev.azure.com/popw/POP%20Forums/_build/latest?definitionId=13)\n\nLatest release:  \nhttps://github.com/POPWorldMedia/POPForums/releases/tag/v22.0.0  \nPackages available on NuGet.\n\nThe latest CI build packages can be found with these feeds on MyGet:  \nhttps://www.myget.org/F/popforums/api/v3/index.json   \n\nSample app using only the packages:  \nhttps://github.com/POPWorldMedia/POPForums.Sample  \n\n## Prerequisites:\n* .NET v10.\n* npm and Node.js to build the front-end.\n* AzureKit optionally requires Redis for two-level cache, Azure Search for Search.\n* AzureKit optionally requires an Azure Storage account for image storage, queues and Azure Functions.\n* ElasticKit optionally requires ElasticSearch for search.\n* Works great on Windows, Mac and Linux.\n* Build with Visual Studio or JetBrains Rider.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurrently being supported with security updates.\n\n| Version     | Supported          |\n| ----------- | ------------------ |\n| 17.x        | :white_check_mark: |\n| 17.99.0.x   | :x:                |\n| < 17.0      | :x:                |\n\n## Reporting a Vulnerability\n\nIf you find something suboptimal, add an issue to the Github project.\n"
  },
  {
    "path": "docs/_config.yml",
    "content": "remote_theme: just-the-docs/just-the-docs@v0.10.1\naux_links:\n  \"POP Forums on GitHub\":\n    - \"https://github.com/POPWorldMedia/POPForums\"\nfooter_content: \"&copy;2025, POP World Media, LLC\"\ntitle: \"POP Forums\"\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "---\nlayout: default\ntitle: Architecture\nnav_order: 2.5\n---\n# POP Forums Architecture\n\nForums are a text-driven medium intended to enable communication between people. To that end, the design philosophy behind POP Forums has always been to keep the interface simple, and not fill the screen with interface elements that don't serve that philosophy. Things that you don't need are hidden by default. Conversation comes first. Peripheral concerns include search engine optimisation, performance and maintainability.\n\nAs an open source project, intended first to be used in community sites like [CoasterBuzz](https://coasterbuzz.com/), its evolution is not perfect, or even ideal, but it does represent two decades of refinement. Some parts are pretty cool, others desperately need to be refactored. No pull request will be ignored!\n\n## Data structure\n\nPOP Forums was originally written using SQL Server as a data store, but the data access bits are contained entirely in the `PopForums.Sql` project. An enterprising developer could easily port this to any of the technologies supported by the .Net ecosystem (which is to say, all of them). This library also contains a basic caching layer, leveraging in-memory cache. It is seeded by a single SQL script, `PopForums.sql`. This is the basic configuration, and it works fine for light duty use on cheap hardware, virtual or otherwise.\n\nTo facilitate scaling, caching can be delegated to Redis, using the `PopForums.AzureKit` library. (Instructions are found in the [Using AzureKit Library](azurekitlibrary.md) section.) This employs a two-level cache that combines the use of Redis and local memory. The app will first attempt to retrieve data from local memory, and if it's not available, it will attempt to retrieve it from Redis. If it's not available there, it will fetch from SQL and then cache that data as apporopriate. This also enables the use of multiple nodes by using the message bus in Redis. When the local cache needs to be invalidated on the other nodes, a message is sent to them via the bus. I didn't invent this pattern, but saw it on a Stack Overflow blog post.\n\nThe basic search functionality is performed by building an enormous table of words, then matching and scoring them for results. The algorithm to index the words is not efficient, but this too works fine for light duty use. Using the `PopForums.ElasticKit` library enables, wait for it, the use of ElasticSearch. No need to invent something, because Elastic does this really well. (Instructions are found in the [Using ElasticKit Library](elastickitlibrary.md) section.) This is a more robust solution that can scale to larger forums, and it can be used in conjunction with the Redis caching layer.\n\nThere is a bit of what I call \"precomputing\" of statistics, because it doesn't make sense to be doing aggregate counts via SQL queries. For example, the post count on a topic is incremented or decremented at the time a post is added or (soft) deleted.\n\n## Background Processing\n\nA number of different tasks are necessarily performed asynchronously:\n* Award Calculation\n* Close Aged Topics\n* Email\n* Post Image Cleanup\n* Search Indexing\n* Subscribe Notifications\n* User Session Cleanup\n\nThe default implementation uses jobs registered as derivatives of `Microsoft.Extensions.Hosting.BackgroundService`. This works fine in a single-node environment, and most of the actions are not resource intensive, save for the search indexing (regardless of using the base search or Elastic).\n\nThe solution for that is to encapsulate the jobs as Azure Functions. These are fantastic in a production situation because you'll literally spend pennies a month on them, while not impacting the resources of your web nodes. If you run on Azure, this is a no-brainer.\n\n## Image Handling\n\nOut of the box, POP Forums stores post images in the database. Streaming those bytes out of SQL Server all the way to the browser is reasonably efficient, but the memory cost is not zero. The advantage of this arrangement is that the forum is very portable, as you can export the database to a `.bacpac` file and restore it, with images, anywhere. User avatars and images are also stored this way.\n\nFor scale, especially if you pay for your database by the bit, it may be preferable to store post images in Azure Blob Storage. If you're using Azure Functions, you'll already need a storage account for the background queues. This too is configured using the `PopForums.AzureKit` library, saving images to a public blob container. This means that you could also use a CDN, if you're really nuts about performance.\n\nBecause a user might upload an image but not submit the post, a cleanup job is run to remove images that are not associated with a post.\n\n## Scaling POP Forums\n\nThe above options allow you to greatly scale the application. Because the app uses ASP.NET's authorization and authentication, running multiple nodes requires a shared data protection key. This is outlined elsewhere and set in the `Program.cs` startup.\n\nUsing the CoasterBuzz database as a reference, three Azure Web App instances, running on P0v3 Linux machines, backed by a 50 DTU SQL database, can handle 1,000 requests per minute without issue. I haven't tested with higher loads and configurations, but it's not clear that either would be a bottleneck running at higher levels.\n\n## The Web Application\n\nAll of the web app code is contained in the `PopForums.Mvc` project, which also references the base `PopForums` and `PopForums.Sql` libraries, and the above-mentioned libraries as necessary for scale. The assets, including all of the CSS and transpiled TypeScript, are also shipped in the `Mvc` library. This makes it really easy to update the forum bits in your own application, without having to replace a bunch of loose files. Update the package, and you're done.\n\nThe app leverages ASP.NET's SignalR for real-time communication with the server via web sockets, including notifications of new posts, updating the forum and topic grids, new private messages, etc.\n\nIn accordance with the simple design philosophy, the web app does not use any specific front-end library, aside from Vue.js, which is used for the admin interface. Again, the intent is to produce search engine friendly markup without a web of dependencies and npm packages. That doesn't mean that there isn't any rich interactivity, because a number of small, raw elements are written in TypeScript. They live in `PopForums.Mvc/Client`. Along with a few small service classes and a simple state engine, \"reactive\" elements are updated when a notification comes in via web sockets.\n\nWhile server-side localization is straight forward enough, the client-side bits use a small JSON payload apply the right language to the interface. For example, the use of time words varies by language, so the `FormattedTime.ts` component uses those strings for \"5 minutes ago\" or whatever the right variant is. \n\n## Unit Testing\n\nThe unit test suite is rough, because there are parts of the app that have not been refactored from an earlier state. A few of the service classes look like dumping grounds, and many do too many things. That makes writing tests for those retroactively difficult, and likely unnecessary if they'll eventually be refactored anyway. You'll also find code in the controllers that likely doesn't belong there. Again, this is after two decades of change. Like any good project, it will never be \"done.\""
  },
  {
    "path": "docs/azurekitlibrary.md",
    "content": "---\nlayout: default\ntitle: Using Azure Kit Library\nnav_order: 6\n---\n# Using AzureKit Library\nThe `PopForums.AzureKit` library makes it possible to wire up the following scenarios:\n* Using Redis for caching (not dependent on Azure specifically... Redis runs everywhere!)\n* Using Azure Storage queues and Functions to queue work for search indexing, emailing and scoring game award calculation\n* Using Azure Storage for image uploads\n* Using Azure Search\n* Using Azure Storage for hosting uploaded images in posts\n* Using Azure Table Storage for error logging\n\nYou don't need to use the AzureKit components to run in an Azure App Service. These components are intended for making scale-out possible. The web app can run self-contained in a single node on an Azure App Service without these optional bits.\n\n## Configuration with Azure App Services and Azure Functions\n\nThe POP Forums configuration system uses the typical configuration files, but adhere's to the overriding system implemented via environment variables. This by extension means that you can set these values in the Application Settings section of the Azure portal for App Services and Functions. It uses the colon notation that you may be familiar with. For example, use `PopForums:Queue:ConnectionString` to correspond to the hierarchy of the `appsettings.json file`. (For Linux-based App Services and Functions in Azure, use a double underscore instead of colons in the portal settings, i.e., `PopForums__Queue__ConnectionString`.)\n\n## Irrelevant settings when using Azure Functions\n\nOnce you get the background stuff out of the web app context, some of the configuration options in the admin are no longer applicable.\n* In email: The sending interval and mailer quantity no longer matter, because the functions only respond when there's something in the queue, and scale as necessary. You may need to limit the number of instances via host.json or the Azure portal if your email service provider throttles your email delivery.\n* In search: The search indexing interval only reacts when something is queued (like email). Furthermore, if you use Azure Search or ElasticSearch, the junk words no longer apply, as these indexing strategies are handled by the appropriate service.\n* In scoring game: The interval is again irrelevant because of the queue.\n\n## Running locally\nYou can almost run everything in this stack locally. Here's the breakdown:\n* Redis is easy to run locally using a Docker container: `docker run -p 6379:6379 -d redis`\n* Azure Storage (for queues) can be simulated locally running [Azurite](https://github.com/azure/azurite) on Windows or Mac (the Azure Storage Emulator has been deprecated). Run this in a Docker container with `docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite`\n* Azure Functions CLI runs on Windows and Mac.\n* Azure Search only runs in Azure.\n* ElasticSearch can run in a Docker container.\n\n## Setting locale in Azure Functions\nSince Azure Functions do not run as a normal web app, listening to the locale of the user's web browser, it defaults to whatever Azure decides is default, probably `en-US` in a lot of places. For some of the functions that are generating notifications, this matters, because you might be serving a Spanish-speaking audience and want them to get notifications in that language.\n\nTo set the language in a function, add the following to those function methods in one of the supported languages:\n```\nThread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(\"es\");\n```\n\n## Using Redis for caching\nRedis is a great tool for caching data for a stand-alone instance of the app or between many nodes. The default caching provided by the `PopForms.Sql` implementation uses in-memory cache in the app instance itself, which doesn't work when you have many nodes (that is, several web heads running behind a load balancer, like a scaled-out Azure App Service). Redis helps by caching data in a \"neutral\" location between these nodes.\n\nTo use Redis (which is available all over the place, and _not_ just in Azure), use the following configuration lines in your ASP.NET `Program.cs`:\n\n```\nvar builder = WebApplication.CreateBuilder(args);\nvar services = builder.Services;\n...\nservices.AddPopForumsRedisCache();\nservices.AddSignalR().AddRedisBackplaneForPopForums();\n...\n```\n\nThe first line configures the app to use the Redis caching mechanism. Under the hood, this replaces the `PopForums.Sql` implementation of `ICacheHelper` with the one found in `PopForums.AzureKit`. The second line adds `AddRedisBackplaneForPopForums()` to the configuration of SignalR, so that websocket messages back to the browser are funneled to clients regardless of which web node they're connected to. For example, a user can make a post connected to one node, and the message to update the recent topics page will be signaled to update for users on every node.\n\nYou'll also need to setup some values in the `appsettings.json` configuration file (or equivalents in your Azure App Service configuration):\n\n```\n{\n  \"PopForums\": {\n    \"Cache\": {\n      \"Seconds\": 180,\n      \"ConnectionString\": \"127.0.0.1:6379,abortConnect=false\",\n      \"ForceLocalOnly\": false\n    },\n...\n```\n\n* `Seconds`: The number of seconds to persist data in the cache.\n* `ConnectionString`: The connection string to the Redis instance.\n* `ForceLocalOnly`: When set to true, the app will not use Redis, just local memory. This might be useful if you scale down to one node and no longer need Redis, but don't want to redeploy, for example. Obviously don't set to true if you're running more than one node.\n\nThis version of `ICacheHelper` is actually a two-level cache. The data is stored locally in the instance memory of the web app, as well as Redis, because it's still faster if it can avoid calling over the wire. Invalidation of data is handled over Redis' built-in communication channels. So if you update something with a particular cache key, Redis will notify all nodes to invalidate any local value they have. Because it's a two-level cache, you might find that your Redis stats seem not to be active, and when it is being called, it's usually a miss. That's because it doesn't have to go to the Redis instance itself, since the value is already in local memory.\n\nThe Azure functions do _not_ use caching. The data that is fetched by the functions is typically transient, not cached or not likely to be.\n\nIf you want to prefix cache keys with a specific string, you can do that by using the dependency injection to replace the default `ITenantService` and implementing its `GetTenant()` method. The default implementation simply returns an empty string. See [Multi-tenant options](multitenant.md) for more information.\n\nTo run Redis locally, consider using Docker. It only takes a few minutes to setup. Use the Google to figure that out.\n\n## Instrumenting Redis and cache usage\n\nMost managed Redis services have ways to generally observe the behavior and health of the service, but you might be interested in going deeper. For example, the default TTL for caching on all of POP Forums is 90 seconds, but that might not be the \"right\" amount of time. Also, because this implementation is a two-level cache, monitoring Redis alone doesn't give you the complete picture. POP Forums has an interface called `ICacheTelemetry` in this library, with a default interface that is just an event sink. If you use an external monitoring service like Azure Insights, you may want to replace this with your own implementation. It's super easy! The interface only has two members:\n\n```\nvoid Start();\nvoid End(string eventName, string key);\n```\n\nThe Redis implementation of `CacheHelper` wraps each call to the memory cache and Redis with the above methods. It includes the cache key and the type of event (`SetRedis`, `GetRedisHit`, `GetRedisMiss`, etc.) for you to persist in whatever your monitoring solution is. In the [hosted forums](https://popforums.com/), we use the following to write the events to Azure Insights. The `TelemetryClient` comes in via dependency injection:\n\n```\npublic class WebCacheTelemetry : ICacheTelemetry\n{\n\tprivate readonly TelemetryClient _telemetryClient;\n\tprivate Stopwatch _stopwatch;\n\n\tpublic WebCacheTelemetry(TelemetryClient telemetryClient)\n\t{\n\t\t_telemetryClient = telemetryClient;\n\t}\n\n\tpublic void Start()\n\t{\n\t\t_stopwatch = new Stopwatch();\n\t\t_stopwatch.Start();\n\t}\n\n\tpublic void End(string eventName, string key)\n\t{\n\t\t_stopwatch.Stop();\n\t\tvar dependencyTelemetry = new DependencyTelemetry();\n\t\tdependencyTelemetry.Name = eventName;\n\t\tdependencyTelemetry.Properties.Add(\"Key\", key);\n\t\tdependencyTelemetry.Duration = new TimeSpan(_stopwatch.ElapsedTicks);\n\t\tdependencyTelemetry.Type = \"CacheOp\";\n\t\t_telemetryClient.TrackDependency(dependencyTelemetry);\n\t}\n}\n```\n\nThen, to wire up this new implementation, we swap out the event sink for our code in `Program.cs`:\n\n`services.Replace(ServiceDescriptor.Transient<ICacheTelemetry, WebCacheTelemetry>());`\n\n## Using Azure Storage queues and Functions\nAzure Storage queues can be used instead of using SQL tables. Using SQL for this is not inherently bad, and honestly the volume of queued things in POP Forums probably never gets huge even on a busy forum, but with queues you get some of the magic of triggering Azure Functions, for example. These are most logically used when you have functions.\n\nTo enable queue usage, use this in your `Program.cs` config:\n\n```\nvar builder = WebApplication.CreateBuilder(args);\nvar services = builder.Services;\n...\nservices.AddPopForumsAzureFunctionsAndQueues();\n...\n```\n\nIt's important to _not_ have `services.AddPopForumsBackgroundServices();` in your `Program.cs`, because this would run the background services in the context of the web app. You don't want that, because you're going to run them in Azure Functions.\n\nYou'll also need to add a connection string to your Azure Storage account and web app service base. These values must appear in the configuration of your web app _and_ Azure Functions.\n\n```\n{\n  \"PopForums\": {\n    \"WebAppUrlAndArea\": \"https://somehost/Forums\",\n    \"Queue\": {\n      \"ConnectionString\": \"DefaultEndpointsProtocol=https;AccountName=youraccountname;AccountKey=xxxYourAccountKeyxxx==\"\n...\n```\nLook at the Azure documentation to see how to provision and deploy Azure Functions, and apply that new knowledge to deploy the `PopForums.AzureKit.Functions` project. (Defining Azure Functions is beyond the scope of this documentation.) You should avoid committing any connection secrets to configuration in source control. See the section above about configuration, and make sure that your Functions have the same settings as your web app.\n\nThe `WebAppUrlAndArea` is used to point the functions back at your web app to notify them as necessary and have them in turn notify users in real-time. The URL should end without a slash, and probably ends in `/Forums` unless you changed the name of the area throughout the code. Behind the scenes, the award calculator uses this to call an endpoint on the web app and let it know that a user has received an award. For security, it uses a hash of the queue connection string, _which must be the same for the web app and the functions_.\n\nThe connection string for using the local Azure storage emulator is `UseDevelopmentStorage=true`.\n\n## Using Azure Search\n\n_Note: v18+ breaks compatibility with previous indexes using Azure Search._\n\nUse this in your `Program.cs` configuration if you're using web in-process search indexing:\n\n```\nvar builder = WebApplication.CreateBuilder(args);\nvar services = builder.Services;\n...\nservices.AddPopForumsAzureSearch();\n...\n```\nUnder the hood, this replaces the `PopForums.Sql` implementation of the search interfaces with those used for Azure Search.\n\nFor use in the Azure functions, you'll need to set the `PopForums:Search:Provider` (or `PopForums__Search__Provider` if it's Linux-based) setting in the portal blade for the functions to `azuresearch`.\n\nYou'll also need to setup the right configuration values:\n\n```\n{\n  \"PopForums\": {\n    \"Search\": {\n      \"Url\": \"https://somesearchservice.search.windows.net\",\n      \"Key\": \"99011A70D3D50D251B0A6141A97B40E7\",\n      \"Provider\": \"\"\n    },\n```\n* `Url`: The URL for Azure Search, typically `https://{nameOfSearchService}.search.windows.net` with the name set in the Azure portal\n* `Key`: A key provisioned by the portal to connect to Azure Search\n* `Provider`: This is only used in `PopForums.AzureKit.Functions`, where it's used to switch between `elasticsearch`, `azuresearch` and the default bits in the `PopForums.Sql` library. _Important: If the value is left blank, the Azure Functions will use the SQL-based search provider._\n\n## Using Azure storage for hosting uploaded images in posts\nThe default implementation for uploading images into forum posts is to upload them into the database. While this is convenient and super portable, it may not be the least expensive option, since database storage is typically more expensive than other means. To that end, you can use `AzureKit` to upload and host the images in an Azure storage container.\n\nThere are a few configuration values you'll need:\n```\n \"PopForums\": {\n    \"BaseImageBlobUrl\": \"http://127.0.0.1:10000/devstoreaccount1\",\n    \"Storage\": {\n      \"ConnectionString\": \"UseDevelopmentStorage=true\"\n    },\n```\n* `BaseImageBlobUrl`: The base URL for the storage where images are uploaded. For local development, using the Azurite storage emulator, this is `http://127.0.0.1:10000/devstoreaccount1`. For a typical Azure storage account, it's probably something like `https://mystorageaccount.blob.core.windows.net`. It should *not* end with a slash, and it shouldn't end with the container name, since that's added in the repository code. In the event you have to move the images for some reason, it's ideal if you could alias a domain name that you own to the storage account.\n* `ConnectionString`: It's assumed that you're going to use the same storage account as your queues, but regardless, you need to specify the connection string here. You need this in the web app *and* the functions app.\n\nThe code will create a container called `postimage` in your storage account, but if you plan to use the public endpoints of the storage account (instead of a CDN), make sure that `Allow blob public access` is enabled for the account, and then when the container is created, it will be created with the `Blob` access level, meaning anyone can access the images, but they can't see the directory or otherwise manipulate the storage account.\n\nYour web app will need to register the right implementation for the `IPostImageRepository`, and this is achieved in your startup/program with this line in the service configuration:\n```\nusing PopForums.AzureKit;\n...\nservices.AddPopForumsAzureBlobStorageForPostImages();\n```\nIt's important to note that `PopForums.AzureKit.Functions` is already wired to use the blob storage `IPostImageRepository` version, because it's assumed that if you're already using an Azure queue, you also have a storage account.\n\nAnother thing to keep in mind is that if you're working locally, and your Azurite instance doesn't have `https` configured, it will break because most browsers do not allow non-`https` images to appear in a secure page. The simplest work around for this is not to install a local certificate, but to change your launch settings for the web app to not run on `https`.\n\n## Using Azure Table Storage for error logging\n\nYou may prefer to do your error logging to Azure Table Storage instead of the SQL database. You can do this by adding one line to your `Program` file, which swaps out the SQL error repository for the table storage:\n```\nservices.AddPopForumsTableStorageLogging();\n```\nThe one limitation here is that you can't use the admin UI to look at errors, since paging and ordering is fairly crude in table storage. For the connection string, it will use whatever is found in `PopForums:Storage:ConnectionString`, the same setting used by the image uploads."
  },
  {
    "path": "docs/customization.md",
    "content": "---\nlayout: default\ntitle: Customization\nnav_order: 2.5\n---\n# Customization\n\nPOP Forums is fairly easy to customize to make it your own. And please, make it your own, because the default Bootstrap style is pretty boring.\n\n## Style\n\nPOP Forums uses [Bootstrap](https://getbootstrap.com/) for its base style. We're big fans of the library because it really does provide a solid starting point and mature components that feel like the _lingua franca_ of web user interfaces. It's also super easy to customize it to your liking with relatively little effort.\n\n### The basic layout that hosts the forum\nThe basic page that we include in the main and sample repositories is fairly sparse:\n```\n<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        <title>@ViewBag.Title</title>\n\n        @await RenderSectionAsync(\"HeaderContent\", false)\n    </head>\n    <body>\n        <div class=\"container\">\n            @RenderBody()\n        </div>\n    </body>\n</html>\n```\nYou'll notice that after the `title` tags, we render a section called `HeaderContent`. This is where the forum drops in all of its `script` and `link` references for Javascript and CSS. If you want to go deeper, look in the `PopForums.Mvc` project at `/Areas/Forums/Views/Shared/PopForumsMaster.csthml`.\n\n### Style overrides\nOne simple approach to customizing the look is to include a style sheet, linked in the template header _after_ Bootstrap. So given the template shown above, you would include your `link` tag to your CSS after the `RenderSectionAsync` call. Mostly, your CSS will consist of things like font selection and colors on high-level elements. For example, given that you've imported fonts elsewhere:\n```\nbody {\n\tfont-family: 'Roboto Slab', 'Times New Roman', serif;\n\tfont-size: 16px;\n}\n\nnav, h1, h2, h3, h4, h5, small, .small, .btn, .breadcrumb {\n\tfont-family: 'Open Sans Condensed', Helvetica, Arial, sans-serif;\n\tfont-weight: 700;\n}\n\na, a:visited, a:hover, .btn-link {\n\tcolor: red;\n\ttext-decoration: underline;\n}\n```\nIf you wanted to override something specific in Bootstrap, like adding a big border to buttons, you would have to make sure you apply an `!important` designation.\n```\n.btn {\n\tborder: solid 2px #000000 !important;\n}\n```\n\n### Bootstrap replacement\nYou can also use your own Bootstrap build and go crazy with customization on the variables. There are great pre-built themes to download from [Bootswatch](https://bootswatch.com/), and [Bootstrap Build](https://bootstrap.build/) has a hassle-free way to customize everything and output a ready-to-use build of Bootstrap. Of course, you can get the [Bootstrap source](https://github.com/twbs/bootstrap) and modify any of the variables or `SCSS` files and build your own, if you like.\n\nTo use your own Bootstrap build from any of the methods above, first you have to turn off the rendering of the default Bootstrap included in the `PopForums.Mvc` package. To do so, you'll need to change the `appsettings.json`:\n```\n{\n  \"PopForums\": { \n    \"RenderBootstrap\": false, \n    ...\n```\nWith that taken care of, considering the template above, add references to your built Bootstrap CSS _and_ script _before_ you render the header content:\n```\n<script src=\"~/pathToMyBootstrap/bootstrap.bundle.js\" asp-append-version=\"true\"></script>\n<link href=\"~/pathToMyBootstrap/bootstrap.min.css\" rel=\"stylesheet\" asp-append-version=\"true\" />\n\n@await RenderSectionAsync(\"HeaderContent\", false)\n```\n\n## Forum adapters\n\nThe MVC project has an interface called `IForumAdapter`, which allows you to generate your own view model for a topic, typically with new or augmented data. When a forum adapter is configured in the admin of the app (on a per-forum basis), it uses the code in that configured adapter to render a specific view (typically in the `Views/Shared` folder of your app) and the model the adapter specifies. Consider the following:\n```\npublic class TestAdapter : IForumAdapter\n{\n    public Task AdaptForum(Controller controller, ForumTopicContainer forumTopicContainer)\n    {\n        // not changing anything in the forum (topic list), just set the model as the existing container\n        Model = forumTopicContainer;\n        return Task.CompletedTask;\n    }\n\n    public async Task AdaptTopic(Controller controller, TopicContainer topicContainer)\n    {\n        // for the topic (thread) view, let's use the existing model, but add something to the `ViewBag` for the view\n        Model = topicContainer;\n        \n        // now get the \"extra\"\n        var resolver = controller.HttpContext.RequestServices;\n        var scoreService = (IScoreService)resolver.GetService(typeof(IScoreService));\n        var highScore = scoreService.GetHighScore();\n        controller.ViewBag.HighScore = highScore;\n        // use Shared/ScoreTopic.cshtml for the view\n        ViewName = \"ScoreTopic\";\n    }\n    ...\n```\nNow a different view will be rendered, and it might look something like this:\n```\n@model PopForums.Models.TopicContainer\n\n<h1>High score: @ViewBag.HighScore</h1>\n\n@foreach(var post in Model.Posts) {\n    // render posts from the model\n```\n\nThis is super flexible, but not without a lot of work. You're highjacking the model from a page like `/Forums/Topic/some-topic`, replacing it with your own, then rendering your own view.\n\nTo light one of these up, you'll have to go to the admin -> Forums -> Edit, and in the bottom field, labeled `Forum Adapter (optional, use \"Namespace.Type, AssemblyName\")`, you should enter what it's asking for. So if the fully qualified name of your adapter is `MyLibrary.MyForumAdapter` in the `MyLibrary` assembly, you would put `MyLibrary.MyForumAdapter, MyLibrary` here."
  },
  {
    "path": "docs/elastickitlibrary.md",
    "content": "---\nlayout: default\ntitle: Using ElasticKit Library\nnav_order: 7\n---\n# Using ElasticKit Library\nThe `PopForums.ElasticKit` library makes it possible to wire up the following scenarios:\n* Use ElasticSearch for search instead of the built-in search indexing. _Important: The client library referenced in v15.x is designed to work against v6.x of ElasticSearch, while v16.x,v17.x and v18.x, v19.x uses v7.x of ElasticSearch. v20.x and v21.x uses v8.x of ElasticSearch. v22.x uses v9.x of ElasticSearch. For prior versions, please see the `docs` folder in those branches. Configuration is significantly different for >=v22.1, and is a breaking change._\n\nElasticSearch can run quite literally anywhere in a docker container or straight up in a VM, if that's your thing. Also keep in mind that the implementation that AWS uses is actually a fork, so there are some differences about how the managed service is, uh, managed. In the commercial hosted version of POP Forums, we use Elastic's managed service running in Azure. Elastic runs in _all_ of the major clouds and is generally reasonably priced.\n\n## Configuration with Azure App Services and Azure Functions\n\nThe POP Forums configuration system uses the `appsettings.json` file, but adhere's to the overriding system implemented via environment variables. This by extension means that you can set these values in the Application Settings section of the Azure portal for App Services and Functions. It uses the colon notation that you may be familiar with. For example, use `PopForums:Queue:ConnectionString` to correspond to the hierarchy of the `appsettings.json` file. (For Linux-based App Services and Functions in Azure, use a double underscore instead of colons in the portal settings, i.e., `PopForums__Queue__ConnectionString`.)\n\n## Irrelevant admin settings when using ElasticKit\n\n* In search: The search indexing interval only reacts when something is queued for in-Web processing, not Azure Functions. Furthermore, if you use ElasticSearch, the junk words no longer apply, as these indexing strategies are handled by ES.\n\n## Using ElasticSearch for search\nElasticSearch is a search engine you can run on your own or in managed services from AWS, Elastic and others. To use this service instead of the internal POP Forums search indexing, you'll need to configure this line in your `Program.cs` if you're using web in-process search processing:\n\n```\nusing PopForums.ElasticKit;\n...\nnamespace YourWebApp;\n\n...\nservices.AddPopForumsElasticSearch();\n```\n\nFor use in the Azure functions, you'll need to set the `PopForums:Search:Provider` (or `PopForums__Search__Provider` on a Linux instance) setting in the portal blade for the functions to `elasticsearch` (see `Provider` config below).\n\nYou'll also need to setup the right configuration values if you're running web in-process:\n\n```\n{\n  \"PopForums\": {\n    \"Search\": {\n      \"Url\": \"https://myelasticsearchindex\",\n      \"Key\": \"\",\n      \"Provider\": \"\"\n    },\n```\n* `Url`: The base URL for the ElasticSearch endpoints. If you're using managed ES from Elastic, this is the \"ElasticSearch Copy endpoint\" result in the portal.\n* `Key`: This is the API key.\n* `Provider`: This is optional in the web app and not actually implemented anywhere other than in our Azure Functions example project, where it's used to switch between `elasticsearch`, `azuresearch` and the default bits in the `PopForums.Sql` library.\n\nConfiguring ElasticSearch and setting up security rules for it are beyond the scope of this wiki.\n"
  },
  {
    "path": "docs/externalloginconfig.md",
    "content": "---\nlayout: default\ntitle: External Login Configuration\nnav_order: 5\n---\n# External Login Configuration\n\n>Important: External logins are not the same as OAuth-Only Mode. Sometimes referred to as \"social logins,\" these are simply a shortcut so your users don't need to remember their forum-specific credentials. They still create an account in the forum. [OAuth-Only Mode](oauthonly.md) relies entirely on an external identity provider and provisions accounts through it.\n> \n> External logins are great for public forums. For corporate or private forums coupled exclusively to an external identity platform, use OAuth-Only Mode.\n\nStarting in v16, POP Forums is completely decoupled from the Identity libraries that verify user identity via third party services, including Google, Facebook and Microsoft. We already don't use Identity because it's so tightly coupled to Entity Framework, with strong opinions about how to store user data. Identity also requires that you configure it at app start (or restart if you change it), and it can't be changed at request time. That prevents a multi-tenancy scenario from working. It was time to cut the cord.\n\nWe spun off the [PopIdentity](https://github.com/POPWorldMedia/POPIdentity) project to be a lightweight, non-opinionated means to do the necessary round trips to identity providers and just give you the data that you want, mostly the ID, name and email of the user. It does not bake the identity into a `Principal` for general use. Check out the sample project there for more information.\n\nIn POP Forums, you can go to the External Logins page of the admin area and configure Google, Facebook, Microsoft and any generic OAuth2 provider that returns JWT's. Check the box, fill in the client ID and secret from the providers. For each, you'll need to specify the callback URL. These are configured:\n* In Facebook's developer administration, under the \"Facebook Login\" and \"Products\" navigation at left.\n* In the Google Cloud Console, drill down to \"Credentials\" under \"API's and Services.\"\n* In Microsoft's Azure portal, search for \"Azure Active Directory,\" choose \"App Registrations,\" choose or create your app, then under \"Authentication\" enter your redirect URL.\n\nThe format for the URL is this, substituting in your domain: `https://whateveryourdomainis/Forums/Identity/CallbackHandler`\n\nWARNING: That URL might be case sensitive in some services. Use the caps in \"Forums\" and such, because that's how the app will generate the URL.\n\nOnce configured, you'll see buttons on the login page for each service you've enabled.\n\nThere's also one for any other OAuth2 provider that returns JWT's. For that option, you'll need to fill in the URL's for the base login URL (POP Forums will append the appropriate query string) and token fetching URL (again, we'll handle the query string)."
  },
  {
    "path": "docs/faq.md",
    "content": "---\nlayout: default\ntitle: FAQ\nnav_order: 3\n---\n# Frequently Asked Questions\n\nThese are a few of the questions people ask me about the project. Feel free to ask other questions in the [GitHub discussions](https://github.com/POPWorldMedia/POPForums/discussions). If you're thinking it, you're probably not the only one! If you find a defect or want to request a feature, use the issue tracker on GitHub for that, please.\n\n## Do I have to pay for this or not?\nNot. POP Forums is an open source software project hosted on GitHub for use under the MIT license. There is a commercially hosted version available at [PopForums.com](https://popforums.com/), yes, for people who don't write code or don't want to mess with managing their own software. Everything on GitHub continues to be open source.\n\n## Another forum app? For real?\nYeah, I know. I'd like to think that this one is a little different, because it doesn't exist to fit some generalized needs, it exists to fit the needs of real communities, like [CoasterBuzz](https://coasterbuzz.com/). The design goal of the app, from its early days in 1999, has always been to design for users, and not be a science project. This app lives because it has been required for sites like CoasterBuzz for more than two decades, and it will continue to evolve because those sites will evolve. It just makes sense to share it with others.\n\n## Sounds like you've been doing this a long time.\nYes, I sometimes feel cursed to rewrite it for all eternity. The Webforms versions were really kind of a mess, and no version was a true rewrite. Once MVC came along, it gave me great incentive to start fresh. Dotnet Core and the evolving front-end frameworks give plenty of new opportunities for refactoring.\n\n## Is this project the basis for the commercial hosted product?\nYes, it's the very same code, though obviously decorated with additional code to facilitate multi-tenancy and provisioning.\n\n## What languages are supported?\nCurrently we have English, Spanish (es), Dutch (nl), Ukrainian (uk), Taiwanese Mandarin (zh-TW) and German (de). If you'd like to translate, the .resx file has around 400 entries. Open an issue to learn more, and we can talk about a pull request to add another language.\n\n## You used to work on the forums for MSDN and TechNet. Is this that forum?\nNot at all. That app served a great many different functions and was integrated with Microsoft ID's, a centralized profiling system, etc. It was/is huge. This app has its roots in the web sites I've been running for fun and profit for years, to the extent that you can find old posts on those sites from the turn of the century with all kinds of formatting failures. Those were the ASP.old days.\n\n## I noticed you're not using [some ORM framework]. Why not?\nOne of the requirements back in the day was to simply work with the existing data structures of v8.x, a Webforms app. In that sense, the data plumbing was already pretty well established and known to work, and it has followed all the way up through the Core version. My opinion is that ORM's tend to be leaky abstractions that never work in the black box way that you would hope. I have adopted Dapper though, which covers the core use case that you're really after anyway: Mapping parameters to queries and results to objects. One doesn't have to write actual SQL all that often, so using an ORM doesn't provide a ton of value.\n\n## You don't name your async methods with the `Async` suffix. Just who do you think you are?\nLook, when almost all of your methods are async with no synchronous version what's the point? The only place I use it is when there are both synchronous and asynchronous methods. Your fancy IDE knows what the return type is, and the compiler lets you know when you're not awaiting. You'll be fine.\n\n## What external frameworks are you using, and why?\nI wanted to keep external binaries to a minimum, but I'm using MailKit for email functions, ImageSharp for photo resizing, NSubstitute for test mocking, and xUnit for unit testing. On the front end, the main app uses vanilla web components written in TypeScript, along with Bootstrap and TinyMCE. The admin area uses Vue.js. Github has that handy dependency graph now that you can look at for more information.\n\n## What? You're not using React?\nHere's the thing about a forum... it's mostly walls of text. I can tell you from the 60,000+ topics I have indexed on a couple of sites that it's super SEO friendly. To that end, the functionality of a forum is mostly making posts, which doesn't require a big library to do. That's why there are little web components spread around on little islands, and not an all-in effort to React. Heck, the admin area uses Vue.js, but even that works by way of a simple script reference, and no transpiling or bundling.\n\n## The unit tests suck.\nThat's not a question. In porting to Core, much of the controller-level unit testing didn't come along, and it needs a lot of refactoring. Ideally, there shouldn't be so much logic in the controllers, but there is still some there.\n\n## What's the release roadmap?\nIt has generally been my intention to keep up with the latest .NET framework versions, which are now reliably annual and released later in the year. You can check the issue tracker for stuff currently in flight. While v19 had a lot of big bang features with a large blast radius, that seems less likely going forward.\n\n## Can I contribute?\nI very much welcome translations of the `.resx` files, so send a pull request for those immediately! If someone really digs into the source code and understands it in a non-trivial way, then yes, I'll happily accept pull requests. If you can find a bug to squish from the issue log, that would be a great PR to see!"
  },
  {
    "path": "docs/features.md",
    "content": "---\nlayout: default\ntitle: Features\nnav_order: 1.5\n---\n# Features\n\n## Classic forum functionality\n* Categories, forums, topics, oh my!\n* Optional Q&A-style threading\n* Direct message, real-time chat between users\n* Upload photos or embed external images\n* Automatically embed YouTube videos\n* Selectively quote previous posts\n* Real-time notifications in-app\n* Rich text editing\n* Avatars and signatures, mutable by users (\"hide vanity\")\n* Recent topics across all forums\n* User profiles with links to social networks\n* Save your favorite topics, subscribe automatically to new post notifications\n* Mark individual or all forums read\n* Jump to the newest post\n* Real-time updating with new posts\n* Continuous scroll topics\n* Vote up and recognize posts\n* Automatic adjustments to display local times\n* Private forums\n* Restrict posts to certain roles by forum\n* Edit posts\n* Ignore users\n* Localized for English, Spanish, German, Dutch, Ukranian and Taiwanese Mandarin\n* Fast page rendering, average 20ms on Azure App Service P0v3 and SQL elastic pool at 50 eDTUs and 900k posts.\n\n## Administration\n* Your own terms of service\n* Adjust number of topics and posts per page\n* Restrict size of uploaded images and YouTube video size\n* Automatically close topics after days of inactivity\n* External (social) logins\n* External identity (All-OAuth mode) that relies on your OAuth/OIDC provider, ideal for enterprise.\n* View detailed security logs\n* Limit posts by time interval in seconds\n* View all recent user sign-ups\n* Error logging and viewing\n* Monitor last run of background services\n* Edit users\n* Email all users\n* Set topic/post page size\n\n## Moderation\n* All-private, sign-up required option\n* E-mail confirmed sign-ups\n* CAPTCHA check\n* Parse out naughty words\n* Assign users to custom roles, limit viewing and posting to those roles by forum\n* Approve profile photos\n* Ban e-mail and IP addresses\n* Edit and soft delete posts, with history\n* Close, pin, move, soft or hard delete topics\n* View moderation logs\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\nlayout: default\ntitle: Home\nnav_order: 1\n---\n![POP Forums logo](https://avatars2.githubusercontent.com/u/8217691?s=200&v=4)\n\nPOP Forums v22 is a forum app for ASP.NET (formerly known as Core), used as the base for several sites maintained by the author, as well as a commercial, cloud hosted product. It's a long-term commitment to great community. If you're looking for the commercial hosted product and support for it, go to [popforums.com](https://popforums.com/).\n\n>_This documentation is for the open source POP Forums project. For documentation about the commercial hosted product, visit [support.popforums.com](https://support.popforums.com/). If you're working with a version that isn't v22, check the `/docs` folder in the source branch that matches the version you're using._\n\nGet a load of [all the features](features.md).\n\nMake test posts and try it out here:  \nhttps://meta.popforums.com/Forums\n\nThe project goals include: \n* Use ASP.NET Core and cloud resources for robust scale-out.\n* Keep the project open source.\n* Be the best and fastest ASP.NET Core-based forum.\n* Not duplicate UBB's 1998 UI for the Nth time.\n* Localize: Now available in English, Spanish, German, Ukrainian, Dutch and Taiwanese Mandarin.\n\nMore information: \n* [FAQ](faq.md)\n* [Architecture](architecture.md)\n* [POP Forums Version History](versionhistory.md)\n* [CI build from `main` in action](https://popforumsdev.azurewebsites.net/Forums)\n* Follow more on [Jeff's blog](https://jeffputz.com/) (may contain autism advocacy and politics)\n\nSetup:\n\nTo set it up, check the installation instructions in the [Start Here](starthere.md) section.\n\nPOP Forums v13 for ASP.NET MVC 5 is also available as a previous release.\n\nDo you speak English and another language? We want to make POP Forums globally useful. Since v9.2, the app is easily localized. Volunteers have translated to Spanish, Dutch, Ukrainian, Taiwanese Mandarin and German, and we'd love help for additional languages. Drop Jeff an e-mail to jeff@popw.com for more information.\n\nFound a bug? Add it to the issue tracker.\n"
  },
  {
    "path": "docs/multitenant.md",
    "content": "---\nlayout: default\ntitle: Multi-tenant options\nnav_order: 8\n---\n# Multi-tenant options\n\nPOP Forums has some plumbing for multi-tenancy, originally created to facilitate the [cloud-hosted version of POP Forums](https://popforums.com/). However, there are some tricks here that you can rely on for shared resource scenarios.\n\n# Using `ITenantService`\n\nThe core library defines `ITenantService` and provides a basic implementation. It has two methods, `SetTenant(string tenantID)` and `GetTenant()`. The former throws a `NotImplementedException` and is not called by any of the code in this repository. It's there for you to use in a true multi-tenant environment. The latter is used all over the place, and the default implementation returns an empty string.\n\n# Example: Sharing an instance of ElasticSearch\n\nLet's say that we have three different sites running their own copy of POP Forums, each with their own database, in a shared pool in Azure. Having multiple databases doesn't cost you anything extra in this scenario, but if you're using managed ElasticSearch hosted by Elastic (also in Azure), you may want to share a single instance. Like many of the resources in POP Forums, the code in [`PopForums.ElasticKit`](elastickitlibrary.md) makes sure to store and query data with a `TenantID`. By default, this doesn't matter, because the ID is just a blank string.\n\nTo share this resource, create an implementation for each of your apps. You'll implement just the `GetTenant()` method:\n```\npublic class TenantService : ITenantService\n{\n    public void SetTenant(string tenantID)\n    {\n        throw new System.NotImplementedException();\n    }\n\n    public string GetTenant()\n    {\n        return \"mytenantid\"; // unique for every app\n    }\n}\n```\nThen, in your `Program.cs` file, swap out the default implementation for your own. If you're using Azure Functions with [`PopForums.AzureKit`](azurekitlibrary.md), be sure to do it in the function project's `Program.cs` as well:\n```\nservices.Replace(ServiceDescriptor.Transient<ITenantService, MyApp.TenantService>());\n```\nIn our ElasticSearch scenario, the indexer will store the ID with every document, and searches will filter by it.\n\n# What uses `TenantID`?\n\nIt's a long list that is best explored in the source code by finding usages of the `GetTenant()` method, but here's a non-exhaustive list:\n\nCore libraries:\n* All of the queue messaging. That means you'll find a TenantID in the code that dequeues the messages (primarily Azure Functions).\n* SignalR plumbing, so clients listening for notifications, for example, are unique to the tenant.\n\nIn `PopForums.AzureKit`:\n* `IPostImageRepository` (used in naming blobs for image storage)\n* All of the Redis bits, so you can share a Redis instance.\n\nIn `PopForums.ElasticKit`:\n* All of the ElasticSearch bits, see example above.\n"
  },
  {
    "path": "docs/oauthonly.md",
    "content": "---\nlayout: default\ntitle: OAuth-Only Mode\nnav_order: 2.7\n---\n# OAuth-Only Mode\n\n>Important: OAuth-Only Mode relies entirely on an external identity provider and provisions accounts through it. [External logins](externalloginconfig.md) are not the same as OAuth-Only Mode. Those are simply a shortcut so your users don't need to remember their forum-specific credentials. They still create an account in the forum.\n>\n> External logins are great for public forums. For corporate or private forums coupled exlcusively to an external identity platform, use OAuth-Only Mode.\n\nStarting in v20, POP Forums has an OAuth-only mode, which means that user authenticaton is handled entirely by a third party. Examples include OAuth providers of corporate identity systems, like Azure Active Directory, Keycloak, Okta and Auth0. In this mode, users can't create an account in the forum, they can only come in via  the external identity provider. The assignment of moderator and admin roles are mapped from claims issued by the identity provider.\n\n> This mode is set at the configuration level (`appsettings.json` locally, or the typical environment variables in regular environments). There are consequences for changing this setting to `true` in an established instance of the forum. Existing users would not be mapped to identities from the external provider. Going the other direction would be possible, though each user would need to reset their password with the email address used by the identity system.\n\nIf you don't have a basic understanding of how OAuth works, now's a good time to do a little research. Here's how this mode works in the forum:\n* The user can only access a page with a single login button.\n* Clicking that button sends the user to the external identity provider.\n* The user enters credentials with the provider if they aren't already logged in. You've likely done this before with \"social\" logins like Google or Facebook.\n* The identity provider redirects the user back to the forum, with a JWT token.\n* The forum calls the identity provider's token endpoint with the provided token and verifies its authenticity.\n* In return, the identity provider returns information about the user called _claims_.\n* The forum checks to see if there's a user account associated with the unique identifier from the provider (given in the `sub` claim). If there is no account, it's created with the provided name and email, otherwise it uses the existing one.\n* The claims are compared to those configured in the forum for moderators and admins, and those roles are assigned to the user if they match.\n* The forum uses an algorithm to reconcile the name and email of the user.\n* The user is then logged in and browsing the forum.\n* After a configured amount of time, the forum will use the refresh token issued by the provider to make sure the user is still legitimate, without the user having to authenticate again.\n\nThis mode uses OpenID Connect (OIDC) claims. The identity provider, in addition to the `sub` claim, must also return a `name` and `email` claim. The provider might need to be configured for this, but those claims should be present if it implements OIDC. We do have to ask for these claims by specifying `scope` in our request, which we'll get to in a minute.\n\n## Configuring your OAuth Provider\n\nThe amount of access and configuration that you have in your identity provider varies a ton. At the very least, the provider should have OIDC enabled, and return `email` and `name` claims. Beyond that, it should return specific claims for forum admins and moderators. The forum can assign these roles based on just the presence of a claim, or by the claim and a specific value.\n\n>In Azure Active Directory, for example, you can create a group and assign members to it. In an app registration, you can configure tokens to return groups as claims. The claims will all be named `roles`, with values that are guids that identify the groups' object ID's. So if a group called \"Forum Admins\" has an object ID of `978efeac-3baf-4e61-a519-9b06eb26a0bf`, the token will have a claim called `roles` with that guid as a value. With other identity providers, it may be possible to simply assign a claim called `ForumAdmin` with no value to represent a forum administrator.\n\nFind out what _scopes_ are required to make sure you're getting the `name` and `email` claims, as well as those that identify your moderators and admins. The typical scope you'll specify, as it relates to the OIDC standard, is:\n```\nopenid email profile offline_access\n```\nThe last one, `offline_access`, is typically required to generate a refresh token, used as described in the flow above.   \n\nFinally, the provider has to know what the valid redirect URL back to the forum is. How you set this varies by identity provider. This follows this pattern:\n```\nhttps://localhost:5091/Forums/Identity/CallbackHandler\n```\nThis is what you would use to redirect to a locally running developer instance of the forum. For real environments, you replace `localhost:5091` with your domain, like `example.com`. Most providers require `https`.\n\n## Configure POP Forums for OAuth-Only\n\nNow that you understand the provider's needs, you can set up the configuration for the forum. These are in addition to the settings described on the [Start Here](starthere.md) page.\n```\n{\n  \"PopForums\": {\n    \"OAuthOnly\": {\n      \"IsOAuthOnly\": true,\n      \"OAuthClientID\": \"provided by identity provider\",\n      \"OAuthClientSecret\": \"provided by identity provider\",\n      \"OAuthLoginBaseUrl\": \"where the forum redirects users\",\n      \"OAuthTokenUrl\": \"where the forum validates tokens\",\n      \"OAuthAdminClaimType\": \"name of the Admin role claim\",\n      \"OAuthAdminClaimValue\": \"optional, value of the Admin role claim\",\n      \"OAuthModeratorClaimType\": \"name of the Moderator role claim\",\n      \"OAuthModeratorClaimValue\": \"optional, value of Moderator role claim\",\n      \"OAuthScopes\": \"openid email profile offline_access\",\n      \"OAuthRefreshExpirationMinutes\": \"60\"\n    }\n  }\n}\n```\nHere's what these do:\n* `IsOAuthOnly`: The master switch for this mode. When set to true, all of the things the forum would do to manage user accounts, like account creation, passwords, email, goes away, delegating it to the OAuth identity provider.\n* `OAuthClientID`: Sometimes called an \"application ID,\" this value comes from the identity provider to understand what service (your forum, in this case) is talking to it.\n* `OAuthClientSecret`: The forum uses this value when it calls back to the provider to validate or refresh a token.\n* `OAuthLoginBaseUrl`: This is the base URL that the forum uses to redirect users to the identity provider. It appends this with a query string, including the callback URL (handled by the forum), the scope, and a state value.\n* `OAuthTokenUrl`: The URL that the forum uses to validate a code or refresh token that came back from the user redirect.\n* `OAuthAdminClaimType`: This is the name of the claim that identifies a user as a forum administrator.\n* `OAuthAdminClaimValue`: This is the value of the above named claim that identifies a user as a forum administrator. If not set, the presence of a `OAuthAdminClaimType` claim with any or no value designates the user as a forum administrator.\n* `OAuthModeratorClaimType`: This is the name of the claim that identifies a user as a forum moderator.\n* `OAuthModeratorClaimValue`: This is the value of the above named claim that identifies a user as a forum moderator. If not set, the presence of a `OAuthModeratorClaimType` claim with any or no value designates the user as a forum administrator.\n* `OAuthScopes`: The scopes to get from the identity provider so that it returns the `sub`, `name` and `email` claims. This is often how you tell the service to return a refresh token as well. Typically, this setting will use `openid email profile offline_access`.\n* `OAuthRefreshExpirationMinutes`: The number of minutes that should pass until the forum asks the identity provider's token endpoint for an updated refresh token. If the user is no longer valid, they will be logged out on their next request. Use a value that is short enough to cause revoked accounts to be shut out, but long enough that every forum request isn't slowed by fetching a refresh token.\n\n## Troubleshooting\n\nErrors should appear right in the user interface. If you need additional context, check the `pf_SecurityLog` table in the database."
  },
  {
    "path": "docs/scoringgame.md",
    "content": "---\nlayout: default\ntitle: The Scoring Game\nnav_order: 9\n---\n# The Scoring Game\n\nLet me tell you a story of HR-discouraged workplace fun. Back in the day, prior to the crash-and-burn of Insurance.com, we had this thing in the development part of the company called the Scoring Game. [I wrote about it](https://jeffputz.com/blog/the-scoring-game) a couple of years ago on my personal blog. The long and short of it is that we kept a running total of +/-1’s for virtually anything you can think of, for each participant. This was back in 2006, before it became trendy to do it for everything else on the Internets.\n\nLater, Digg started doing all kinds of voting, and it was really the first active example that I can think of that I used in terms of measuring value of content (yes, slashdot did it, but I never went there). Various forums started doing it. StackOverflow based much of its value on a scoring system, along with achievements. When I worked at Microsoft, I worked on the reputation system that feeds the various MSDN properties. It seems inevitable that I’d have to add something like this to POP Forums.\n\nOriginally, I was thinking just in terms of voting up posts, but then I realized that there were actually two things to build. The voting mechanism was one part, but the actual scoring was a second part that should be decoupled from the voting. So the workflow goes like this:\n\nProcess Event –> Publish to user profile (optional) –> Get associated awards –> Qualify awards –> Give award\n\nTo use the system, you only need only a few lines of code. Use the dependency injection to get the implementation of `PopForums.ScoringGame.IEventPublisher`. If you're using the typical constructor injection, you'll probably have a reference to it in your class called `_eventPublisher`. The user can be obtained from an instance of `PopForums.Mvc.Areas.Forums.Services.IUserRetrievalShim` in the controller layer.\n\n```c#\n_eventPublisher.ProcessEvent(\"message for feed\", user, \"TestEventID\", false);\n```\n\nPretty simple, eh? The first string is the text that will be published to the user’s feed (if the event is set to publish), the second is the `PopForums.Models.User` object to associate with the event, and the third is the actual event ID. Event definitions are really simple.\n\nThere are three events that are static, permanently built into the system. These are wired into the post voting, and the creation of new topics and posts. So for example, when someone votes up a post, a string of HTML is passed in to the ProcessEvent() method, with the user object associated with the post, and the event ID PostVote.\n\nEvents don’t have to be published to the user’s profile, and they don’t even need to assign points. New posts and topic events fall into this category. So what’s the point then? Awards! POP Forums leaves that up to you. Award definitions are super simple as well. We can assign any combination of events to the award.\n\nThat’s really all there is to it. You can set up stuff anywhere in your app to record events, and publish them to the user profile. Give points, give awards. Knock yourself out!"
  },
  {
    "path": "docs/starthere.md",
    "content": "---\nlayout: default\ntitle: Start Here\nnav_order: 2\n---\n# Start Here\n\nPOP Forums attempts to not get in the way of your application, by working as an MVC area. All of the front-end dependencies are embedded in the Nuget packages, so there's no need to npm packages and build and copy stuff.\n\nHow to use [The Scoring Game](scoringgame.md) in your own application.\n\n## Upgrading?\n\nThis version has data changes. From v21.x, run the `PopForums21to22.sql` script included in the `PopForums.Sql` project. If you need to upgrade from v16.x to v20.x, _first_ run the `PopForums16to20.sql` script against your database, which is found in the `PopForums.Sql` project. It's safe to run this script more than once. IMPORTANT: Going from v18 forward, because of the changes to private messages, you must first also delete all of the existing history by running `DELETE FROM pf_PrivateMessage` against your database. The reason that this isn't included in the upgrade script is because you should know it's necessary and do it on your own.\n\nUpdating your app from the legacy ASP.NET MVC world to ASP.NET Core is non-trivial, and well beyond the scope of this documentation.\n\n## Prerequisites\nYou'll need the following locally:\n* Visual Studio 2026 or later, with the Azure workload (community version is fine), Visual Studio for Mac or Jetbrains Rider\n* Node.js (comes with npm)\n* SQL Server Developer, or SQL Server running in a Docker container\n* A mail sending service that supports SMTP\n* Optionally, Docker if you intend to run Azurite, Redis, ElasticSearch, etc. (instructions below)\n\n## Build vs. reference\n\nYou should definitely get to know the installation information below to understand the project structure, but understand that you can also use POP Forums by way of Nuget package references. The [POPWorldMedia/POPForums.Sample](https://github.com/POPWorldMedia/POPForums.Sample) project shows how you can do this without having to build this project, and it's literally adding a Nuget package reference and adding some config stuff in `Program.cs`.\n\n## Reference\n\n* Again, [POPWorldMedia/POPForums.Sample](https://github.com/POPWorldMedia/POPForums.Sample) is a good starting point when using POP Forums via reference, but the `PopForums.Web` project in the source code works similarly, with project references instead of NuGet references.\n* Reference `PopForums.Mvc` and `PopForums.Sql` from Nuget. If you want to use the scale-out kits (`PopForums.AzureKit` and `PopForums.ElasticKit`), add those as well.\n* Starting with v19, `PopForums.Mvc` includes all of the front-end goodies, the Javascript and CSS, right in the package.\n* You'll need a layout view for the forum to live in.\n* Set up the various options in `Program.cs` as described in its comments and this documentation.\n* `appsettings.json` will have your forum configuration.\n* There is no package for the Azure Functions, because it's currently hard to make them work from a shared library in certain situations. However, you can deploy the project from the main repo with ease given the tooling in VS or Azure DevOps Pipelines. Just be sure to set the right values up in the application configuration in the Azure portal.\n* POP Forums uses ASP.NET Data Protection in multi-node or external login scenarios. Actually, the basic anti-forgery code baked into the framework does as well, so when you deploy, or swap deployment slots in Azure, you need to persist the underlying key somewhere. This is also true if you run multiple nodes (scale out). You can persist the underlying keys in a number of different ways (I prefer Azure Blob Storage). In your `Program.cs`, use `services.AddDataProtection()` and the appropriate extension method. If you don't do this for multi-node, things like social logins and anti-forgery will fail and fill your error logs with stuff about broken things. If you use slots in Azure App Services, you'll also want the Data Protection setup, otherwise the swap will cause everyone to be logged out.\n\nFor the bleeding edge, latest build from `main`, the CI build packages can be obtained by a MyGet feed:\n* https://www.myget.org/F/popforums/api/v3/index.json (Nuget package includes the server application and front-end assets)\n\n## Build\n\n* Clone the latest source code from GitHub, or use the production packages as described above. Build it. If your IDE doesn't automatically build Javascript or Typescript, be sure to `npm install` in the `PopForums.Mvc` project, and then run the `gulpfile.js`.\n* The project files require an up-to-date version of Visual Studio 2026 or later, but it also works great with Jetbrains' Rider on Mac or Windows. I prefer it.\n* This project is built on ASP.NET v10. Make sure you have the required SDK installed (v10.0.100).\n* The `PopForums.Web` project is the template to use to include the forum in your app. It references `PopForums.Mvc`, which contains all of the web app-specific code, including script and CSS. `PopForums.Sql` concerns itself only with data, while `PopForums` works entirely with business logic and defines interfaces used in the upstream projects. `PopForums.AzureKit` contains a number of items to facilitate using various Azure services. `PopForums.ElasticKit` contains an ElasticSearch implementation. `PopForums.AzureKit.Functions` is an implementation of functions, used if you're not using in-app context background services (see below).\n* The `main` branch is using Azure Functions by default to run background processes. Run the [Azurite](https://github.com/azure/azurite) container in Docker (works on Windows and Mac). If not, you can run the background things in-process by uncommenting `services.AddPopForumsBackgroundServices()` in `Program.cs` and commenting out or removing `services.AddPopForumsAzureFunctionsAndQueues()`. This causes all of the background things to run in the context of the web app itself.\n\n> Running the background services in the web context can cause some wild variations in CPU and RAM usage on a busy forum, especially in the code associated with updating the search index. If you are running in Azure, using Functions is a much better choice for consistent and predictable app performance.\n\n## Installation\n\n* Once you've completed one of the above scenarios, reference or build, it's time to fire it up, starting with the configuration file.\n* `appsettings.json`, in the root of the web project, is the basic configuration file for POP Forums. It works like any other config file in ASP.NET Core, so when you're running in Azure, you can use the colon notation in the App Service application settings to set these values (i.e., `PopForums:Cache:Seconds` as the key).\n\n> If you run the app in a Linux App Service or container, your settings notation should replace `:` with a double underscore, `__`. So the above would be `PopForums__Cache__Seconds`.\n\n```js\n{\n    \"PopForums\": {\n        \"IpLookupUrlFormat\": \"https://whatismyipaddress.com/ip/{0}\", // used on Recent Users screen of admin to lookup IP addresses\n        \"BaseImageBlobUrl\": \"http://127.0.0.1:10000/devstoreaccount1\", // if using AzureKit to host images, points to the base URL of images uploaded to blob storage (you should really alias the storage to a domain you own)\n        \"Storage\": {\n            \"ConnectionString\": \"UseDevelopmentStorage=true\" // if using AzureKit to host images, typically the same as the Queue:ConnectionString, but the place where images are uploaded to blob storage\n        },\n        \"Database\": {\n            \"ConnectionString\": \"server=localhost;Database=popforums21;Trusted_Connection=True;TrustServerCertificate=True;\"\n        },\n        \"Cache\": {\n            \"Seconds\": 180,\n            \"ConnectionString\": \"127.0.0.1:6379,abortConnect=false\", // used for Redis cache in AzureKit\n            \"ForceLocalOnly\": false // used for Redis cache in AzureKit\n        },\n        \"Search\": { // used for Elastic or Azure Search (see docs)\n            \"Url\": \"popforumsdev\",\n            \"Key\": \"99011A70D3D50D251B0A6141A97B40E7\",\n            \"Provider\": \"\"\n        },\n        \"Queue\": { // used for queues with Azure Functions\n            \"ConnectionString\": \"UseDevelopmentStorage=true\"\n        },\n        \"LogTopicViews\": true, // optional, records topic views for future analytics\n        \"ReCaptcha\": { // Google ReCaptcha on signup (the key/secret below works on localhost)\n            \"UseReCaptcha\": true,\n            \"SiteKey\": \"6Lc2drIUAAAAAPaa1iHozzu0Zt9rjCYHhjk4Jvtr\",\n            \"SecretKey\": \"6Lc2drIUAAAAADXBXpTjMp67L-T5HdLe7OoKlLrG\"\n        },\n        \"WebAppUrlAndArea\": \"https://somehost/forums\", // used only by Azure Functions to find endpoint of your web app\n        \"RenderBootstrap\": true, // optional, defaults to true, put false here if your host page will have its own build of Bootstrap CSS\n        \"OAuthOnly\": {\n            // this section is detailed in the OAuth-Only Mode section\n        }\n    }\n}\n```\n\n* Attempt to run the app locally via Kestrel, and go to the URL `/Forums` to see an error page about not finding the settings table. It will fail either because the database isn’t set up, or because it can’t connect to it. The biggest reason for failure is an incorrect connection string. If you change nothing locally, by default it's looking for a local database on the default SQL Server instance called `popforums21`.\n* If you want to use the setup page (and you should), don’t run the SQL script. Once the POP Forums tables exist in the database, the setup page will tell you that you’re prohibited from going there.\n* Point the browser to `/Forums/Setup` now, and if your connection string is correct, you should see a page with some of the basic fields to set up.\n> If you're running in OAuth-Only Mode, there is no setup for the fields below. The forum will attempt to set up the database, and that's it. That mode has no email functionality, and user creation and roles are delegated to the external identity provider. See [OAuth-Only Mode](oauthonly.md) for more information.\n* The `PopForums.Mvc` package includes Bootstrap, which is used as the base style for the entire app. To give it your own look, you can add your own CSS to override Bootstrap in your `_Layout.cshtml`, or do your own build of Bootstrap with whatever variables you like. If you prefer your own build, make sure _both_ the Javascript and CSS tags appear _before_ the `RenderSection` in your header, and set the `RenderBootstrap` setting in `appconfig.json` to `false`. Learn more in [customization](customization.md).\n* If you're using Azure functions in the background, instead of embedding the background work in the web app (see [Using AzureKit](azurekitlibrary.md)), you'll want to run multiple startup projects, specifically the `PopForums.Web` and `PopForums.AzureKit.Functions`.\n\nHere’s what each field on the setup page does: \n* **Forum title:** This is what your forum will be called at the root, in an h1 tag. You can edit this (and everything else) later.\n* **SMTP Server:** The host name of the server you’ll connect to for sending e-mail. Enabling this functionality on your server is beyond the scope of this document, but we usually use SendGrid to send email.\n* **Port:** Typically 25, though some services (like Gmail) use others.\n* **From e-mail address:** When a user receives e-mail from the forum, it will be “from” this address.\n* **Use SSL:** Check if your server uses or requires SSL.\n* **Use ESMTP for credentials:** Check this box if you have to authenticate with your server (this is almost always the case). Checking this makes the two boxes below it editable.\n* **SMTP User:** User name (often the e-mail address) to authenticate with. Not editable unless the “Use ESMTP” box is checked.\n* **SMTP Password:** Password to authenticate with. Not editable unless the “Use ESMTP” box is checked.\n* **Display name:** How you want your name to appear in the forum.\n* **E-mail:** The e-mail address you’ll use to login with.\n* **Password:** The password you’ll use to login with.\n\nYou're almost there!\n\n* If you typed everything you need correctly, you should see a happy result, otherwise you’ll see a stack trace and exception.\n* Restart the app.\n* From here, you can follow the link to the admin home page and add categories and forums. You’ll be logged in as the user you created, and that account will be part of the Admin and Moderator roles.\n* Once you’ve added some forums on the “Forums” admin page, you can go to `/Forums` to start posting.\n* If you want to test your e-mail setup, go to `/Forums/Account/Forgot` and enter your e-mail address. Failures are also logged in the error log, which is found in the admin area.\n* For future reference, you can revisit the admin area at `/Forums/Admin`, and when you're logged in as an admin, a link appears in the user dropdown from the navigation menu.\n\n## Integration\n\nThe `PopForums.Web` project is the template you can use as the basis for your own POP Forums apps. If you want to build via the most recent stable builds, the [POPWorldMedia/POPForums.Sample](https://github.com/POPWorldMedia/POPForums.Sample) project is an example of how to do that (see above). The app uses the standard claims-based authentication, but it does not use Identity or Entity Framework. When you're logged in, you'll find the identity of the user on the User property of the controller as expected. The `PopForumsAuthorizationMiddleware` loads user and profile data into the request pipeline, so it can be loaded once and used throughout the request lifecycle.\n\nAccessing the user data can be achieved via an instance of `IUserRetrievalShim`, which you can inject into your dependency chain. From a controller, simply call `_userRetrievalShim.GetUser()` to get the fully hydrated POP Forums `User`, or `_userRetrievalShim.GetProfile()` to get the profile. Under the hood, these are stored in the `Items` collection of the current `HttpContext`.\n\nThe easiest way to integrate with an existing set of users is to connect via an OAuth2 provider. Read more about [OAuth-Only Mode](oauthonly.md).\n\n## Running third-party services in Docker containers\n\nIf you want to run locally with some of the \"kits\" described in the documentation, you'll need to fire them up using Docker. Here are the commands for the most common things. These sometimes change, because of new names, versions and such, but they're current as of early 2023.  \n  \n* SQL Server (keep in mind that `mcr.microsoft.com/azure-sql-edge` is the ARM versoin of SQL)  \n`docker run --cap-add SYS_PTRACE -e 'ACCEPT_EULA=1' -e 'MSSQL_SA_PASSWORD=P@ssw0rd' -p 1433:1433 --name sqledge -d mcr.microsoft.com/azure-sql-edge`  \n* Azureite, for storage and queues  \n`docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite`  \n* Redis, for distributed cache and SignalR backplane  \n`docker run --name some-redis -p 6379:6379 -d redis`  \n* ElasticSearch, for better search  \n`docker run --name es-9 -p 9200:9200 -e discovery.type=single-node -it docker.elastic.co/elasticsearch/elasticsearch:9.3.0`  \n\nYou may want to have your databases be more durable in the event you trash the SQL container or update to a new one. To do that, first create a new volume, either in Docker Desktop or on the command line:\n```\ndocker volume create sqldata\n```\nThen fire up the container and associate it with the volume, and tell it to use that volume for all of the data.\n```\ndocker run -e 'ACCEPT_EULA=1' -e 'MSSQL_SA_PASSWORD=P@ssw0rd' -p 1433:1433 --name sqledge -v sqldata:/DATA -d mcr.microsoft.com/azure-sql-edge\n```\nIf you would like to host the data files in your own file system, you can start the container like this, replacing the approprirate paths to your local spots, where `<host directory>` is your spot:\n```\ndocker run -e 'ACCEPT_EULA=1' -e 'MSSQL_SA_PASSWORD=P@ssw0rd' -p 1433:1433 --name sql2022 -v <host directory>:/var/opt/mssql/data -d mcr.microsoft.com/mssql/server:2022-latest\n```\nAnd if you need to copy files out of an existing container, you can do that too. `~/sqlvolumes` in this case points to a folder in my user folders on a Mac:\n```\ndocker cp containerID3bed54c7734b:/var/opt/mssql ~/sqlvolumes\n```\n\n## Running Azure Functions on a Mac\n\nThis isn't the most straightfoward thing, and it's hard to find the information, but you need to install the Azure Functions Core tools via Homebrew. [Microsoft explains how to do this.](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Cmacos%2Ccsharp%2Cportal%2Cbash#install-the-azure-functions-core-tools)\n\n## Customization\n\nTo make POP Forums look the way you want, or with extra functionality, read up on [customization](customization.md).\n"
  },
  {
    "path": "docs/versionhistory.md",
    "content": "---\nlayout: default\ntitle: Version History\nnav_order: 4\n---\n# POP Forums Version History\n\nHere's a partial version history that shows how POP Forums has evolved over the years. It's fun to look back at some of the things we now take for granted in a forum app.\n\n## Version v21.1.0 (PopForums.ElasticKit only, 12/16/25)\n* The ElasticSearch key value in the configuration should simply be the API key, which is a more modern convention used in Elastic's cloud service.\n\n## Version v22.0.0 (11/24/25)\n* Implement an ignore feature #318\n* BUG: YouTube short links throwing exception on submit #386\n* BUG: Hosted service failure in startup will prevent app start #381\n* BUG: In process hosted jobs run at silly intervals #383\n\n## Version v21.0.1 (PopForums.Mvc only, 12/8/24)\n* BUG: Hosted service failure in startup will prevent app start #381\n* BUG: In process hosted jobs run at silly intervals #383\n\n## Version v21.0.0 (12/7/24)\n* Update IForumAdapter to use async methods (breaking change) #361\n* Clean up naming and organization of Authorization and Authentication bits #377\n* Remove Twitter from profiles #372\n* Migrate to .Net 9 and new libraries #371\n* Refactor in-process background services to run on IHostedService #357\n* Use streams when reading image data from SQL #359\n* Refactor settings caching to make it simple #370\n* BUG: Valid emails are being rejected #367\n* BUG: PopForumsAuthorizationIgnoreAttribute doesn't actually cause middleware to skip user hydration #376\n* BUG: GetUsersByPointTotals won't serialize for caching #358\n* BUG: New PM incorrectly shows as \"user not found\" #360\n* BUG: URL ending in closing parentheses breaks links #375\n* BUG: Duplicate ForumsUserIDType claim being added to identity in SignalR hub #378\n* BUG: Sequence contains no elements exception thrown in middleware #379\n\n## Version v20.0.0 (12/8/23)\n* Allow option to rely entirely on 3rd-party OAuth2 and OIDC for sign-in #183\n* Vote up buttons need an interim state during call #334\n* Update ElasticKit to use v8.x #335\n* Updating to TypeScript 5.x requires accommodation of suppressImplicitAnyIndexErrors deprecation #345\n* Animate notification and PM count so you see it #327\n* Reduce size of trimmed URL's to fit in mobile situations #323\n* Delete queued email messages after send #145\n* Update to .NET v8 #348\n* Update Polly to v8.x (in ElasticKit) #352\n* Replace Moq with NSubstitute #340\n* User creation should be atomic with profile, not sourced in a controller #328\n* Refactor out the old FullUrlHelper #326\n* PopForumsUserAttribute needs to be refactored to async #347\n* BUG: Fix regex not escaping special characters #342\n* BUG: Close private chat if the other user is deleted #321\n* BUG: Chromium 114 causes element property name conflict #336\n* BUG: Associate external login failing #324\n* BUG: Load more before reply chokes when only one page of posts #319\n* BUG: Images uploaded in post edit are flagged for deletion #316\n\n## Version v19.0.2 (6/6/23)\n* BUG: Chromium 114 causes element property name conflict #336\n\n## Version v19.0.1 (1/29/23)\n* BUG: Images uploaded in edit are flagged for deletion #316\n* BUG: Load more before reply chokes when only one page of posts #319\n* BUG: Associate external login failing #324\n\n## Version v19.0.0 (10/9/22)\n* Image upload in posts #109\n* Create notification system #265\n* Change PM system to real-time chat #304\n* Refine post quoting to reduce full-post quotes #283\n* Clean up old notifications #298\n* Refresh and expand iconography #300\n* Migrate from email based subscriptions to in-app notification #264\n* Remove email subscriptions #276\n* Update PM count in real-time #275\n* New setting: reply-to email address #252\n* Offer view of recent users in admin for spam monitoring #258\n* Don't show user profiles for unverified accounts #263\n* Include prompts to verify account where necessary #266\n* Allow vote up reversal #287\n* Use a reactive state box as base for new features #269\n* Remove composite activity feed #299\n* Use fulltext editor for profile signatures #291\n* Decommission email from profile #310\n* Add option to write error log to table storage #314\n* Refactor: Move all of the client stuff to the `Mvc` project, ditch the npm package #285\n* Refactor: Update MailKit library to v3.x #268\n* Refactor: Update ImageSharp library to v2.x #267\n* Refactor: Update TinyMCE library to v6.x #274\n* Refactor: DateTime parsing on client using server localization, with DST #188\n* Refactor: Create proper client-side localization mechanism #293\n* Refactor: Migrate old PopForums.js to a more manageable, not-spaghetti, TypeScript base #286\n* Refactor: Clean up on aisle admin #295\n* Refactor: Exclude bots from session tracking #301\n* Refactor: Signalr hub consolidation #312\n* BUG: New `SqlCommand` can't parse setup script without semicolons #288\n* BUG: Scroll to newest misses target because of inline images loading #290\n* BUG: Infinite scroll repositions back to hash when more posts load #257\n* BUG: New topic or reply fails if using Azure queues and they fail #284\n\n## Version v18.0.2 (5/28/22)\n* BUG: Fix bug in newer version of SqlClient when running setup script. #288\n\n## Version v18.0.1 (1/8/22)\n* BUG: Fix auto scroll when new posts load. #257\n\n## Version 18.0.0 (11/8/21)\n\n* Update to .NET 6. #211\n* Migrate to Bootstrap v5. #215\n* Eliminate dependencies on jQuery. #216\n* Migrate admin to Vue.js v3. #232\n* Embrace C# concepts. #249\n* Use crop mode for avatars when they're uploaded. #239\n* Update text parser to default to https on www URL's. #248\n* Trim IP and email ban entries before saving. #222\n* Remove dependency on Newtonsoft serializer for controllers with enums. #247\n* Add a security policy. #238\n* Update `AzureKit` to use modern libraries. #226\n* Put a cache sink in Redis `CacheHelper` in `AzureKit`. #231\n* Update Redis `CacheHelper` to use `System.Text.Json` when it's compatible. #169\n* Discontinue use of `PopForums.json` for config, use default instead. #225\n* BUG: Mailing list unsubscribe doesn't decode spaces from URL's. #237\n* BUG: CSS needs case sensitive path for Linux environments. #228\n* BUG: `SmtpWrapper` doesn't catch connections or auth exceptions. #237\n* BUG: Bots causing null ref exception in `TopicViewCountService` when trying to fetch cookies. #210\n* BUG: Setup stopped working in a previous version. #244\n* BUG: Text parser fails on URL's with multiples protocol instances (like those from web archive). #233\n\n## Version 17.0.2 (9/29/21)\n\n* Fix for broken setup bug. #244\n\n## Version 17.0.1 (4/4/21)\n\n* Fix for multiple image bug. #234\n\n## Version 17.0.0 (8/24/20)\n\n* Rename AwsKit project to ElasticKit. #184\n* Add alerts for admin errors. #152\n* Use the right Bootstrap color context on email verification. #195\n* Add configuration for entirely private forum. #86\n* Use the HTML quoting in post edit box instead of forum tags. #201\n* Generate site maps for all topics. #199\n* Audit CSS and use Bootstrap when possible. #186\n* Search for user on enter on admin user edit page. #194\n* Use hashing mechanism for topic unsubscribes, the way mailing list works. #189\n* Migrate to `Microsoft.Azure.Storage` packages from `Microsoft.WindowsAzure.Storage`. #182\n* Use Bootstrap hidden utilities to hide elements on mobile. #181\n* Move recent and feed links to navigation bar. #187\n* Embrace `IHttpContextAccessor` in user shim. #176\n* Add a telemetry interface for Redis cache, but don't implement it. #185\n* Use contrasting colors for new post indicators. #179\n* Refresh user profiles. #174\n* Improve ElasticSearch for proper resiliency, add API key ability. #173\n* Log search service errors. #171\n* BUG: User profile tabs have wrong background for activity and awards. #172\n* BUG: Avatars and signatures not appearing for anonymous user. #197\n* BUG: Pasted URL's paste as links, parser can't shorten the text. #163\n* BUG: Formatting in quoted posts sometimes causes portions of post to get parsed out. #198\n* BUG: LastTopicView could have duplicate entries. #202\n\n## Version 16.0.1 (12/27/19)\n\n* BUG: Redis cache helper can't serialize forum view/post graphs. #168\n\n## Version 16.0.0 (12/17/19)\n\n* Update to .Net Core v3.1. #151\n* Redis connection failures recorded as \"information\" instead of \"error.\" #162\n* Optimize user objects and caching. #44\n* Using \"page\" in routes can potentially interfere with Razor pages. #150\n* Close dormant threads on a background job. #67\n* Allow topic author to edit title. #80\n* Refactor all the things to async/await. #132\n* Refactor external logins to decouple from Identity (uses [POP Identity](https://github.com/POPWorldMedia/POPIdentity)). #140\n* Cleanup startup configuration. #129\n* Refactor the posting service classes. #121\n* Cleanup unused methods and tests. #133\n* Add unique constraints for user name and email. #144\n* Add ReCAPTCHA option for login. #143\n* Update to a stronger hash algorithm for passwords. #142\n* Reintroduce profile caching. #148\n* BUG: Failed reply doesn't wire up error message and attempts to redirect. #160\n* BUG: Delete, undelete, hard delete doesn't remove from search index. #154\n* BUG: Give background job to delete indexed words in self-rolled search more time. #149\n* BUG: Creation of Redis connection not thread safe, possible race condition. #135\n* BUG: Delete/undelete does not trigger a search reindex. #136\n* BUG: Search indexer Azure function does not respect settings for provider setting. #137\n* BUG: External login list has duplicate entries for description. #139\n\n## Version 15.0.0 (5/27/19)\n\n* General update of dependencies.\n* Rewrite of admin to use Vue.js. #120\n* Scaffolding for recording view data, correlating between users and topics (for future analytic use). #104\n* Optionally run background processes as Azure functions. #76\n* Social icons in profile. #119\n* Redis backplane for SingalR in multi-node hosting. #64\n* Migrated to Bootstrap v4.x. #94\n* Abandon decade-old constructors in models. #96\n* Remove first post preview from topic lists (no one used it). #97\n* Truncate error log instead of delete all. #103\n* Adopt Dapper usage in `PopForums.Sql` library. #105\n* Improve performance for IP history and security log. #106\n* Realign social links in profile to modern services. #107\n* Provide a standard way to fail distributed events. #117\n* Add support for ElasticSearch. #116\n* BUG: Over-zealous regex hangs unparse of client HTML. #118\n* BUG: Fix Favorite & Subscription Topic pages. #111\n* BUG: It's possible to submit empty posts with just returns. #82\n* BUG: TinyMCE is mangling hyperlinks. #125\n* BUG: TinyMCE is inserting extra attributes in image tags, breaking parsing. #128\n\n## Version 14.1.0 (12/9/18)\n\n* Update to .Net Core v2.2.\n* General package updates.\n* Move all MVC views to a project that can be published as a Razor class library package.\n* Remove ability to resize images in TinyMCE editor, since attributes are ignored anyway.\n* Bug: for pager links in faves and subs (#90 and #91).\n\n## Version 14.0.0 (7/15/18)\n\n* This is a port of v13 to ASP.NET Core v2.1.0. It's mostly intended to achieve feature parity.\n* Experimental AzureKit allows for multi-instance use and scaling.\n\n## Version 13.0.0 (2/14/15)\n\n* Completely revised UI uses Bootstrap, replaces separate mobile views.\n* New Q&A style forums.\n* Preview your posts for formatting.\n* Social logins using OWIN 2.x.\n* StructureMap replaces Ninject for dependency injection.\n* Admins can permanently delete a topic.\n* Facebook and Twitter links added to profiles.\n* IP ban works on partial matches.\n* Bug: Initial user creation didn't salt passwords ([Codeplex 131](http://popforums.codeplex.com/workitem/131))\n* Bug: Replies not triggering reindex for topics (#4).\n* Bug: LastReadService often called from Post action of ForumController without user, throws null ref (#1).\n* Bug: Reply and quote buttons appear in posts even when the topic is closed (#8).\n* Bug: When email verify is on, changing email does not set IsApproved to false (#10).\n* Bug: Image controller throws when Googlebot sends a weird If-Modified-Since header (#13).\n* Bug: Reply or new topic can be added to archived forum via direct POST outside of UI (#15).\n* Bug: Multiple entries to last forum and topic view tables causing exception when reading values into dictionary (#17).\n* Experimental: Support for multiple instances in Azure with shared Redis cache (not production ready).\n\n## Version 12.1.0 (4/25/14)\n\n* Added Taiwanese Mandarin translation\n* Fixed HtmlHelper to remove reference to DependencyResolver. #122: HtmlHelper for role checkboxes referenced static DependencyResolver\n* Disable submit buttons on new topics/replies for mobile views. #121: Port the submit button disable for posts in mobile view\n* Fixed mobile view of edit doesn't parse text for overridden mobile mode. #123: Mobile post edit view has HTML, save doesn't persist line breaks\n\n## Version 12.0.0 (12/8/13)\n\n* Updated to use .NET 4.5.1 and MVC 5, including the latest library code (jQuery, SignalR, OWIN, etc.)\n* User passwords now include per-user salt, backward compatible to existing user data. #120: Salt passwords by user\n* External logins implemented via OWIN, for Google, Facebook, Twitter and Microsoft accounts. #117:Integrate the OWIN external auth stuff\n* YouTube URL's not formatted as hyperlinks converted to embedded video, provided images are allowed in posts. #116: Parse YouTube URL's to convert into YouTube iframes\n* Controller dependencies converted to private members. #115: Refactor controller dependencies to private members\n\n## Version 11.1.0 (9/4/13)\n\n* Added Ukrainian to supported languages. Also includes English, Spanish, German and Dutch.\n* Fixed issue #115: Mobile view allows double post.\n\n## Version 11.0.1 (5/15/13)\n\n* Fix for issue #113: User can post in closed topic via mobile views.\n\n## Version 11.0.0 (4/17/13)\n\n* Updated to use v4.5 of .NET.\n* External references now use NuGet.\n* Adding an award definition in admin now bounces you to its edit page.\n* Fixed: Show more posts updates topic context with updated page counts.\n* Activities and awards restyled.\n* User profiles are tabbed.\n* Activity feed shows real-time view of activity sent via the scoring game API.\n* Times are updated every minute, formatted to current culture.\n* More posts are loaded on scroll (a la Facebook), but pager links are maintained for search engine discoverability.\n* New posts appear inline at end of post stream as they're made.\n* Forum home and individual topic lists updated in real time.\n* Breadcrumb/navigation floats at top of browser.\n* .forumGrid CSS removes outline, so it's more Metro-y.\n\n## Version 10.0.1 (9/15/12)\n\nThis update has no UI component or data changes. It only addresses the following bug:\n* ServiceModule has potential race condition in ASP.NET v4.5\n* A bug in the MVC 4 framework requires this package for mobile views: http://nuget.org/packages/Microsoft.AspNet.Mvc.FixedDisplayModes\n\n## Version 10.0.0 (8/16/12)\n\n* Uses a very light weight CSS and Javascript package to provide a touch-friendly interface for mobile devices.\n* Numbers are formatted (sensitive to culture) when 1,000 or higher.\n* CSS is more integration friendly, and specific to the ForumContainer element.\n* Mail delivery from queue is now parallel, so you can specify a sending interval, and the number of messages to process on each interval.\n* Background \"services\" refactored, and will only run with a call on app start to PopForumsActivation.StartServices(). This is partly to facilitate future use in Web farms/multiple Web roles in Azure.\n* Update to jQuery v1.7.1.\n* Replaced use of .live() with .on() in script, pursuant to jQuery update, which deprecates .live().\n* Renamed HomeController to ForumHomeController, to make lives easier when integrating into an MVC app.\n* Dependency resolution no longer requires that you set Ninject as the container for the entire MVC app. The controllers now resolve their dependencies in their constructors, so you're free to set up any DI container in your global.asax.\n* The included single-server SQL data layer now uses the base classes and interfaces for (DbConnection, DbCommand, etc.) instead of the specific SQL flavors, for easier refactoring in case you want to build an Oracle version or something.\n* FIX: Bug in topic repository around caching keys for single-server data layer.\n* FIX: Pager links on recent topics pointed to incorrect route.\n* FIX: Deleting a post didn't update last user/post time.\n* FIX: Ditched attempt at writing to event log with super failures, since almost no one has permission in production.\n* FIX: Bug in grayed-out fields in admin mail setup.\n* FIX: Weird color profiles would break loading of images for resize.\n* FIX: TOS text on account sign-up was double encoded.\n\n## Version 9.2.1 (1/26/12)\n\n* Added Spanish (es) translation.\n\n## Version 9.2.0 (1/23/12)\n\n* Localization: The app can be easily translated using .resx files. Initially includes English (en), German (de) and Dutch (nl).\n* Vote up posts: Give credit to people who make good posts, see who voted up each one.\n* The scoring game: Extensible system that allows you to give users points, and issue awards based on repeated events. For example, you can set up awards based on the number of new posts or topics a user makes (both of which are recorded).\n* Fix: Weird line breaks in lists when posting from Firefox.\n\n## Version 9.1.1 (12/18/11)\n\n* Corrects a bug that prevented a topic from being marked viewed when a user looked at it, resulting in \"new post\" indicators that didn't go away until the user marked the entire forum as read.\n\n## Version 9.1.0 (12/15/11)\n\n* New \"adapter\" interface for forums. Using the IForumAdapter interface, a developer can plug-in code that alters the model and/or resulting view on topic lists and the actual threads. For example, you might add to or alter the model, then present a different view to display the data. See the comments on the IForumAdapter interface for more information.\n* Also new, users starting a reply will see a button indicating that they can load any new posts that have occurred since they started writing their apply, so they don't miss any of the conversation.\n* Fix: Moderating topic title doesn't update the UrlName.\n* SEO enhancement: Page links in topics and forums include rel=\"next\" and rel=\"prev\" to tell search engines there's more to look at.\n* Fix: User post list had broken markup, preventing topic preview.\n* Fix: Added missing permission checking on action methods to preview or load individual posts.\n* User name in top nav now acts as a link to the user's profile.\n* Fix: Cache key for caching view post roles was incorrect.\n\n## Version 9.0.0 (4/24/11)\n\n* Rewritten from scratch for ASP.NET MVC3, with reasonable test coverage\n* Posts can be loaded inline\n* Avatars and user images resized on server\n\n## Version 8.5.0 (not publicly released)\n\n* Added plug-in infrastructure to allow changes to UI. Made for photo forum on CoasterBuzz\n\n## Version 8.0.0 (11/10/08)\n\n* Added AJAX features, takes advantage of ASP.NET v3.5.\n\n## Version 7.5.1 (2/9/05)\n\n* Significantly altered the TextParser class\n* Changed text in RegisterLogin.ascx to indicate e-mail is used to send activation code\n* Topic titles now parsed for naughty words and HTML\n\n## Version 7.5.0 (10/25/04)\n\n* New text parsing engine\n* New online user engine\n* RichText control displays in Windows 2000\n* Fixed member post paging\n* Online user stats now draw from PopForums.OnlineUsers\n* Added RequiredFieldValidator to SendPrivateMessage.ascx to check for a subject\n* PM reply doesn't add endless string of \"re:\"\n* Fixed PagerLinks.cs to correctly display tool tips\n* Added PagedPagerLinks.cs to display paged results from new PopForums.Forum.GetTopics() overload\n* Made the member mailer text box in the admin bigger\n* Can't reply to closed topics, even if reply box was visible before submitting\n\n## Version 7.0.3 (1/24/04)\n\n* Fixed URL and image detection in TextParser.\n\n## Version 7.0.2 (11/11/03)\n\n* Updated the RichText class to make it compatible with a recent update to Internet Explorer.\n\n## Version 7.0.1 (11/4/03)\n\n* Removed license scheme to make POP Forums free.\n\n## Version 7.0.0 (10/22/03)\n\n* Total rewrite with separation of data, logic and user interface.\n* Several user interfaces available at launch.\n* Data caching can reduce database activity by 60% or more.\n* A RichText server control that renders in Internet Explorer.\n* Role-based security you can use anywhere on your site, based on ASP.NET's forms authentication.\n* Discreet data class, so you can write your own to access any database. (SQL Server used by default.)\n* A TimeAdjust class to convert times to local time zones, and even adjust for daylight savings.\n* Text parsing engine takes care of HTML generated by RichText as well as traditional \"forum code.\"\n\n## Version 6.0.0 (7/1/02)\n\n* Original ASP application ported to ASP.NET.\n* Significant improvements to HTML parsing engine.\n* Forum exists as a user control, just drop it in your .aspx page!\n* E-mail notification for anyone who replies to a topic.\n* All-CSS interface shipped along side \"admin-able\" formatting.\n* Asynchronous mailing to opt-in members (no need to tie up your browser with mailing process).\n* Last post indicated by member name.\n* Scroll to new post bounces you to page with newest post.\n* Role-based security. Only those in certain roles can access or post in the forums you designate.\n* Robust forum property and ordering in admin area.\n* Error logging to database.\n\n## Version 5.1 (3/4/02)\n\n* New HTML/forum code parsing engine.\n\n## Version 5.0 (8/19/01)\n\n* Support for SQL Server Full-Text Indexing built-in for much faster searching.\n* WYSIWYG post editor for Internet Explorer users, normal for other browsers.\n* Favorite topic list.\n* Terms of Service agreement with first post, reset all members' agreement.\n* Member post quotas, to limit \"noisy\" members.\n* Support for Persits and Dimac mail components.\n* Post preview.\n* \"Jump to\" added to topic and thread pages to jump to another forum.\n* Public stats shown optionally (number of sessions, topics, members, etc.)\n* Client-side form validation of new topics or replies to challenge empty fields.\n* Admin edit member accounts.\n* Recent topics shows links to multi-paged topics.\n* Error page for URL's of deleted topics and forums.\n* Fixed edit page hang for long posts.\n* Search only returns private forum results to those who have access.\n* Removed \"requires AspMail\" from admin page.\n* Moderator IP view spans all pages in thread.\n* Manage e-mail notification page reformatted.\n* Double carriage returns now parsed as p tags.\n* Fixed CDONTS mailer for compatibility with settings of other components.\n* Fixed br unparsing with forum code off.\n\n## Version 4.0 (1/8/01)\n\n* Added support for CDO, CDONTS and AspQMail components.\n* Post new topic and reply links added to topic and thread pages.\n* COPPA check defeat.\n* Support for Spellchecker.net.\n* Scroll to newest post link.\n* Addition of pre and quote forum tags.\n* Paged threads. Posts per page are admin-defined. Links to each page appear in topic list.\n* Auto-login after first post.\n* AIM and ICQ hyperlinks in member info.\n* Non-logged-in forums indicates \"register to track new posts.\"\n* COPPA (Children's Online Privacy Protection Act) compliance.\n* Moderators can view IP addresses of posters.\n* E-mail notification of new posts when member starts thread.\n* Member area to remove e-mail notification of new posts.\n* Opt-out or delete account option built into special URL sent with mass mailings to members.\n* Mail to friend now uses system e-mail contact as from address, member as replyto address (to handle SMTP authentication issues).\n* Moving lone topic from forum no longer results in an error.\n\n## Version 3.2 (11/12/00)\n\n* Included explanation of smilies in help file.\n\n## Version 3.1 (9/20/00)\n\n* One-click close/open/delete threads for moderators.\n* Parse ftp:// URL's.\n* Post-and-close thread check box for moderators,\n* Next/previous thread.\n* Mail thread to a friend.\n* Recent topics.\n* Fixed left column text color in search results.\n\n## Version 3.0 (8/22/00)\n\n* Major rewrite of most code.\n* Dozens of files consolidated.\n* Made to be portable between systems.\n* Extensive administration area.\n* Custom formatting added to instantly change fonts, colors, etc.\n* Search engine added.\n* Private and archived forums added.\n\n## Version 2.0 (1/31/00)\n\n* Written for CoasterBuzz.\n* Cleaner format.\n* Forum off/on.\n* \"Mark posts read\" time stored in profile instead of browser.\n* Signature block added.\n* Profile photo upload added.\n\n## Version 1.0 (11/23/99)\n\n* Written for Guide to The Point.\n* Basic features only.\n"
  },
  {
    "path": "src/.editorconfig",
    "content": "# EditorConfig is awesome:http://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Don't use tabs for indentation.\n[ \"*\" ]\nindent_style = tab\nend_of_line = lf\ninsert_final_newline = true\n# (Please don't specify an indent_size here; that has too many unintended consequences.)\n\n# Code files\n[*.{cs,csx,vb,vbx,cshtml}]\nindent_style = tab\nindent_size = 4\n\n# Xml project files\n[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]\nindent_style = tab\nindent_size = 2\n\n# Xml config files\n[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]\nindent_style = tab\nindent_size = 2\n\n# JSON files\n[*.json]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": "src/PopForums/Composers/ForumStateComposer.cs",
    "content": "﻿namespace PopForums.Composers;\n\npublic interface IForumStateComposer\n{\n\tForumState GetState(Forum forum, PagerContext pagerContext);\n}\n\npublic class ForumStateComposer : IForumStateComposer\n{\n\tpublic ForumState GetState(Forum forum, PagerContext pagerContext)\n\t{\n\t\tvar forumState = new ForumState {ForumID = forum?.ForumID, PageSize = pagerContext.PageSize, PageIndex = pagerContext.PageIndex};\n\t\treturn forumState;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Composers/PrivateMessageStateComposer.cs",
    "content": "﻿namespace PopForums.Composers;\n\npublic interface IPrivateMessageStateComposer\n{\n\tTask<PrivateMessageState> GetState(PrivateMessage pm);\n}\n\npublic class PrivateMessageStateComposer : IPrivateMessageStateComposer\n{\n\tprivate readonly IPrivateMessageService _privateMessageService;\n\n\tpublic PrivateMessageStateComposer(IPrivateMessageService privateMessageService)\n\t{\n\t\t_privateMessageService = privateMessageService;\n\t}\n\n\tpublic async Task<PrivateMessageState> GetState(PrivateMessage pm)\n\t{\n\t\tvar state = new PrivateMessageState();\n\t\tvar messages = await _privateMessageService.GetMostRecentPosts(pm.PMID, pm.LastViewDate);\n\t\tstate.NewestPostID = messages.Any() ? messages.First().PMPostID : 0;\n\t\tvar bufferMessages = await _privateMessageService.GetPosts(pm.PMID, pm.LastViewDate);\n\t\tmessages.InsertRange(0, bufferMessages);\n\t\tstate.PmID = pm.PMID;\n\t\tvar clientMessages = ClientPrivateMessagePost.MapForClient(messages);\n\t\tstate.Messages = clientMessages;\n\t\tstate.Users = pm.Users;\n\t\tvar pmUsersFromPMRecord = pm.Users.Deserialize<List<object>>();\n\t\tvar pmUsers = await _privateMessageService.GetUsers(pm.PMID);\n\t\tvar isUserNotFound = pmUsers.Count != pmUsersFromPMRecord.Count;\n\t\tstate.IsUserNotFound = isUserNotFound;\n\t\treturn state;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Composers/ResourceComposer.cs",
    "content": "﻿namespace PopForums.Composers;\n\npublic interface IResourceComposer\n{\n\tdynamic GetForCurrentThread();\n}\n\npublic class ResourceComposer : IResourceComposer\n{\n\tpublic dynamic GetForCurrentThread()\n\t{\n\t\tvar resources = new\n\t\t{\n\t\t\tLessThanMinute = Resources.LessThanMinute,\n\t\t\tOneMinuteAgo = Resources.OneMinuteAgo,\n\t\t\tMinutesAgo = Resources.MinutesAgo,\n\t\t\tTodayTime = Resources.TodayTime,\n\t\t\tYesterdayTime = Resources.YesterdayTime,\n\n\t\t\tNotifications = Resources.Notifications,\n\t\t\tNewReplyNotification = Resources.NewReplyNotification,\n\t\t\tAward = Resources.Award,\n\t\t\tVoteUpNotification = Resources.VoteUpNotification,\n\t\t\tQuestionAnsweredNotification = Resources.QuestionAnsweredNotification,\n\t\t\tSend = Resources.Send,\n\n\t\t\tUploadImage = Resources.UploadImage\n\t\t};\n\t\treturn resources;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Composers/TopicStateComposer.cs",
    "content": "﻿namespace PopForums.Composers;\n\npublic interface ITopicStateComposer\n{\n\tTask<TopicState> GetState(Topic topic, int? pageIndex, int? pageCount, int lastVisiblePostID);\n}\n\npublic class TopicStateComposer : ITopicStateComposer\n{\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly ISubscribedTopicsService _subscribedTopicsService;\n\tprivate readonly IFavoriteTopicService _favoriteTopicService;\n\n\tpublic TopicStateComposer(IUserRetrievalShim userRetrievalShim, ISettingsManager settingsManager, ISubscribedTopicsService subscribedTopicsService, IFavoriteTopicService favoriteTopicService)\n\t{\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_settingsManager = settingsManager;\n\t\t_subscribedTopicsService = subscribedTopicsService;\n\t\t_favoriteTopicService = favoriteTopicService;\n\t}\n\n\tpublic async Task<TopicState> GetState(Topic topic, int? pageIndex, int? pageCount, int lastVisiblePostID)\n\t{\n\t\tvar topicState = new TopicState {TopicID = topic.TopicID, PageIndex = pageIndex, PageCount = pageCount, LastVisiblePostID = lastVisiblePostID, AnswerPostID = topic.AnswerPostID};\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user != null)\n\t\t{\n\t\t\ttopicState.IsImageEnabled = _settingsManager.Current.AllowImages;\n\t\t\ttopicState.IsFavorite = await _favoriteTopicService.IsTopicFavorite(user.UserID, topic.TopicID);\n\t\t\ttopicState.IsSubscribed = await _subscribedTopicsService.IsTopicSubscribed(user.UserID, topic.TopicID);\n\t\t}\n\n\t\treturn topicState;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Configuration/Config.cs",
    "content": "﻿namespace PopForums.Configuration;\n\npublic interface IConfig\n{\n\tstring DatabaseConnectionString { get; }\n\tint CacheSeconds { get; }\n\tstring CacheConnectionString { get; }\n\tbool ForceLocalOnly { get; }\n\tstring SearchUrl { get; }\n\tstring SearchKey { get; }\n\tstring QueueConnectionString { get; }\n\tstring SearchProvider { get; }\n\tbool LogTopicViews { get; }\n\tbool UseReCaptcha { get; }\n\tstring ReCaptchaSiteKey { get; }\n\tstring ReCaptchaSecretKey { get; }\n\tstring IpLookupUrlFormat { get; }\n\tstring WebAppUrlAndArea { get; }\n\tstring BaseImageBlobUrl { get; }\n\tstring StorageConnectionString { get; }\n\tbool RenderBootstrap { get; }\n\tbool IsOAuthOnly { get; }\n\tstring OAuthClientID { get; }\n\tstring OAuthClientSecret { get; }\n\tstring OAuthLoginBaseUrl { get; }\n\tstring OAuthTokenUrl { get; }\n\tstring OAuthAdminClaimType { get; }\n\tstring OAuthAdminClaimValue { get; }\n\tstring OAuthModeratorClaimType { get; }\n\tstring OAuthModeratorClaimValue { get; }\n\tstring OAuthScopes { get; }\n\tint OAuthRefreshExpirationMinutes { get; }\n}\n\npublic class Config : IConfig\n{\n\tpublic Config(IConfiguration configuration)\n\t{\n\t\tif (_configContainer == null)\n\t\t{\n\t\t\tvar loader = new ConfigLoader();\n\t\t\t_configContainer = loader.GetConfig(configuration);\n\t\t}\n\t}\n\n\tprivate static ConfigContainer _configContainer;\n\n\tpublic string DatabaseConnectionString => _configContainer.DatabaseConnectionString;\n\tpublic int CacheSeconds => _configContainer.CacheSeconds;\n\tpublic string CacheConnectionString => _configContainer.CacheConnectionString;\n\tpublic bool ForceLocalOnly => _configContainer.CacheForceLocalOnly;\n\tpublic string SearchUrl => _configContainer.SearchUrl;\n\tpublic string SearchKey => _configContainer.SearchKey;\n\tpublic string QueueConnectionString => _configContainer.QueueConnectionString;\n\tpublic string SearchProvider => _configContainer.SearchProvider;\n\tpublic bool LogTopicViews => _configContainer.LogTopicViews;\n\tpublic bool UseReCaptcha => _configContainer.UseReCaptcha;\n\tpublic string ReCaptchaSiteKey => _configContainer.ReCaptchaSiteKey;\n\tpublic string ReCaptchaSecretKey => _configContainer.ReCaptchaSecretKey;\n\tpublic string IpLookupUrlFormat => _configContainer.IpLookupUrlFormat;\n\tpublic string WebAppUrlAndArea => _configContainer.WebAppUrlAndArea;\n\tpublic string BaseImageBlobUrl => _configContainer.BaseImageBlobUrl;\n\tpublic string StorageConnectionString => _configContainer.StorageConnectionString;\n\tpublic bool RenderBootstrap => _configContainer.RenderBootstrap;\n\tpublic bool IsOAuthOnly => _configContainer.IsOAuthOnly;\n\tpublic string OAuthClientID => _configContainer.OAuthClientID;\n\tpublic string OAuthClientSecret => _configContainer.OAuthClientSecret;\n\tpublic string OAuthLoginBaseUrl => _configContainer.OAuthLoginBaseUrl;\n\tpublic string OAuthTokenUrl => _configContainer.OAuthTokenUrl;\n\tpublic string OAuthAdminClaimType => _configContainer.OAuthAdminClaimType;\n\tpublic string OAuthAdminClaimValue => _configContainer.OAuthAdminClaimValue;\n\tpublic string OAuthModeratorClaimType => _configContainer.OAuthModeratorClaimType;\n\tpublic string OAuthModeratorClaimValue => _configContainer.OAuthModeratorClaimValue;\n\tpublic string OAuthScopes => _configContainer.OAuthScopes;\n\tpublic int OAuthRefreshExpirationMinutes => _configContainer.OAuthRefreshExpirationMinutes;\n}\n"
  },
  {
    "path": "src/PopForums/Configuration/ConfigContainer.cs",
    "content": "﻿namespace PopForums.Configuration;\n\npublic class ConfigContainer\n{\n\tpublic string DatabaseConnectionString { get; set; }\n\tpublic int CacheSeconds { get; set; }\n\tpublic string CacheConnectionString { get; set; }\n\tpublic bool CacheForceLocalOnly { get; set; }\n\tpublic string SearchUrl { get; set; }\n\tpublic string SearchKey { get; set; }\n\tpublic string QueueConnectionString { get; set; }\n\tpublic string SearchProvider { get; set; }\n\tpublic bool LogTopicViews { get; set; }\n\tpublic bool UseReCaptcha { get; set; }\n\tpublic string ReCaptchaSiteKey { get; set; }\n\tpublic string ReCaptchaSecretKey { get; set; }\n\tpublic string IpLookupUrlFormat { get; set; }\n\tpublic string WebAppUrlAndArea { get; set; }\n\tpublic string BaseImageBlobUrl { get; set; }\n\tpublic string StorageConnectionString { get; set; }\n\tpublic bool RenderBootstrap { get; set; }\n\tpublic bool IsOAuthOnly { get; set; }\n\tpublic string OAuthClientID { get; set; }\n\tpublic string OAuthClientSecret { get; set; }\n\tpublic string OAuthLoginBaseUrl { get; set; }\n\tpublic string OAuthTokenUrl { get; set; }\n\tpublic string OAuthAdminClaimType { get; set; }\n\tpublic string OAuthAdminClaimValue { get; set; }\n\tpublic string OAuthModeratorClaimType { get; set; }\n\tpublic string OAuthModeratorClaimValue { get; set; }\n\tpublic string OAuthScopes { get; set; }\n\tpublic int OAuthRefreshExpirationMinutes { get; set; }\n}\n"
  },
  {
    "path": "src/PopForums/Configuration/ConfigLoader.cs",
    "content": "﻿namespace PopForums.Configuration;\n\npublic class ConfigLoader\n{\n\tpublic ConfigContainer GetConfig(IConfiguration configuration)\n\t{\n\t\tvar container = new ConfigContainer();\n\t\tcontainer.DatabaseConnectionString = configuration[\"PopForums:Database:ConnectionString\"];\n\t\tvar cacheSeconds = configuration[\"PopForums:Cache:Seconds\"];\n\t\tcontainer.CacheSeconds = string.IsNullOrEmpty(cacheSeconds) ? 90 : Convert.ToInt32(cacheSeconds);\n\t\tcontainer.CacheConnectionString = configuration[\"PopForums:Cache:ConnectionString\"];\n\t\tcontainer.CacheForceLocalOnly = Convert.ToBoolean(configuration[\"PopForums:Cache:ForceLocalOnly\"]);\n\t\tcontainer.SearchUrl = configuration[\"PopForums:Search:Url\"];\n\t\tcontainer.SearchKey = configuration[\"PopForums:Search:Key\"];\n\t\tvar searchProvider = configuration[\"PopForums:Search:Provider\"];\n\t\tcontainer.SearchProvider = searchProvider ?? string.Empty;\n\t\tcontainer.QueueConnectionString = configuration[\"PopForums:Queue:ConnectionString\"];\n\t\tvar logTopicViews = configuration[\"PopForums:LogTopicViews\"];\n\t\tcontainer.LogTopicViews = logTopicViews != null && bool.Parse(logTopicViews);\n\t\tvar useReCaptcha = configuration[\"PopForums:ReCaptcha:UseReCaptcha\"];\n\t\tcontainer.UseReCaptcha = useReCaptcha != null && bool.Parse(useReCaptcha);\n\t\tcontainer.ReCaptchaSiteKey = configuration[\"PopForums:ReCaptcha:SiteKey\"];\n\t\tcontainer.ReCaptchaSecretKey = configuration[\"PopForums:ReCaptcha:SecretKey\"];\n\t\tcontainer.IpLookupUrlFormat = configuration[\"PopForums:IpLookupUrlFormat\"];\n\t\tcontainer.WebAppUrlAndArea = configuration[\"PopForums:WebAppUrlAndArea\"];\n\t\tcontainer.BaseImageBlobUrl = configuration[\"PopForums:BaseImageBlobUrl\"];\n\t\tcontainer.StorageConnectionString = configuration[\"PopForums:Storage:ConnectionString\"];\n\t\tvar renderBootstrap = configuration[\"PopForums:RenderBootstrap\"];\n\t\tcontainer.RenderBootstrap = renderBootstrap != null ? bool.Parse(renderBootstrap) : true;\n\t\tvar isOAuthOnly = configuration[\"PopForums:OAuthOnly:IsOAuthOnly\"];\n\t\tcontainer.IsOAuthOnly = isOAuthOnly != null ? bool.Parse(isOAuthOnly) : false;\n\t\tcontainer.OAuthClientID = configuration[\"PopForums:OAuthOnly:OAuthClientID\"];\n\t\tcontainer.OAuthClientSecret = configuration[\"PopForums:OAuthOnly:OAuthClientSecret\"];\n\t\tcontainer.OAuthLoginBaseUrl = configuration[\"PopForums:OAuthOnly:OAuthLoginBaseUrl\"];\n\t\tcontainer.OAuthTokenUrl = configuration[\"PopForums:OAuthOnly:OAuthTokenUrl\"];\n\t\tcontainer.OAuthAdminClaimType = configuration[\"PopForums:OAuthOnly:OAuthAdminClaimType\"];\n\t\tcontainer.OAuthAdminClaimValue = configuration[\"PopForums:OAuthOnly:OAuthAdminClaimValue\"];\n\t\tcontainer.OAuthModeratorClaimType = configuration[\"PopForums:OAuthOnly:OAuthModeratorClaimType\"];\n\t\tcontainer.OAuthModeratorClaimValue = configuration[\"PopForums:OAuthOnly:OAuthModeratorClaimValue\"];\n\t\tcontainer.OAuthScopes = configuration[\"PopForums:OAuthOnly:OAuthScopes\"];\n\t\tvar refreshMinutes = configuration[\"PopForums:OAuthOnly:OAuthRefreshExpirationMinutes\"];\n\t\tcontainer.OAuthRefreshExpirationMinutes = string.IsNullOrEmpty(refreshMinutes) ? 60 : Convert.ToInt32(refreshMinutes);\n\t\t\n\t\treturn container;\n\t} \n}"
  },
  {
    "path": "src/PopForums/Configuration/ErrorLog.cs",
    "content": "namespace PopForums.Configuration;\n\npublic interface IErrorLog\n{\n\tvoid Log(Exception exception, ErrorSeverity severity);\n\tvoid Log(Exception exception, ErrorSeverity severity, string additionalContext);\n\tList<ErrorLogEntry> GetErrors(int pageIndex, int pageSize, out PagerContext pagerContext);\n\tPagedList<ErrorLogEntry> GetErrors(int pageIndex, int pageSize);\n\tTask DeleteError(int errorID);\n\tTask DeleteAllErrors();\n}\n\npublic class ErrorLog : IErrorLog\n{\n\tpublic ErrorLog(IErrorLogRepository errorLogRepository)\n\t{\n\t\t_errorLogRepository = errorLogRepository;\n\t}\n\n\tprivate readonly IErrorLogRepository _errorLogRepository;\n\n\tpublic void Log(Exception exception, ErrorSeverity severity)\n\t{\n\t\tLog(exception, severity, null);\n\t}\n\n\tpublic void Log(Exception exception, ErrorSeverity severity, string additionalContext)\n\t{\n\t\tif (exception != null && exception is ErrorLogException)\n\t\t\treturn;\n\t\tvar message = string.Empty;\n\t\tvar stackTrace = string.Empty;\n\t\tvar s = new StringBuilder();\n\t\tif (additionalContext != null)\n\t\t{\n\t\t\ts.Append(\"Additional context:\\r\\n\");\n\t\t\ts.Append(additionalContext);\n\t\t\ts.Append(\"\\r\\n\\r\\n\");\n\t\t}\n\t\tif (exception != null)\n\t\t{\n\t\t\tmessage = exception.GetType().Name + \": \" + exception.Message;\n\t\t\tif (exception.InnerException != null)\n\t\t\t\tmessage += \"\\r\\n\\r\\nInner exception: \" + exception.InnerException.Message;\n\t\t\tstackTrace = exception.StackTrace ?? string.Empty;\n\t\t\tforeach (DictionaryEntry item in exception.Data)\n\t\t\t{\n\t\t\t\ts.Append(item.Key);\n\t\t\t\ts.Append(\": \");\n\t\t\t\ts.Append(item.Value);\n\t\t\t\ts.Append(\"\\r\\n\");\n\t\t\t}\n\t\t}\n\t\ts.Append(\"\\r\\n\");\n\t\tvar moreData = s.ToString();\n\t\ttry\n\t\t{\n\t\t\t// TODO: Eventually make this async, but its web of call stacks are huge\n\t\t\t_errorLogRepository.Create(DateTime.UtcNow, message, stackTrace, moreData, severity);\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\tthrow new ErrorLogException($\"Can't log error: {message}\\r\\n\\r\\n{stackTrace}\\r\\n\\r\\n{moreData}\");\n\t\t}\n\t}\n\n\tpublic List<ErrorLogEntry> GetErrors(int pageIndex, int pageSize, out PagerContext pagerContext)\n\t{\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar errors = _errorLogRepository.GetErrors(startRow, pageSize).Result;\n\t\tvar errorCount = _errorLogRepository.GetErrorCount().Result;\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(errorCount) / Convert.ToDouble(pageSize)));\n\t\tpagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\treturn errors;\n\t}\n\n\tpublic PagedList<ErrorLogEntry> GetErrors(int pageIndex, int pageSize)\n\t{\n\t\tvar errors = GetErrors(pageIndex, pageSize, out PagerContext pagerContext);\n\t\tvar list = new PagedList<ErrorLogEntry> { PageCount = pagerContext.PageCount, PageIndex = pagerContext.PageIndex, PageSize = pagerContext.PageSize, List = errors };\n\t\treturn list;\n\t}\n\n\tpublic async Task DeleteError(int errorID)\n\t{\n\t\tawait _errorLogRepository.DeleteError(errorID);\n\t}\n\n\tpublic async Task DeleteAllErrors()\n\t{\n\t\tawait _errorLogRepository.DeleteAllErrors();\n\t}\n}"
  },
  {
    "path": "src/PopForums/Configuration/ErrorLogException.cs",
    "content": "namespace PopForums.Configuration;\n\npublic class ErrorLogException : Exception\n{\n\tpublic ErrorLogException(string message) : base(message)\n\t{\n\t\t\n\t}\n\n\tpublic override string Message => \"Can't log exception: \" + base.Message;\n}"
  },
  {
    "path": "src/PopForums/Configuration/ErrorSeverity.cs",
    "content": "namespace PopForums.Configuration;\n\npublic enum ErrorSeverity\n{\n\tCritical = 1,\n\tWarning = 2,\n\tInformation = 3,\n\tDebug = 4,\n\tError = 5,\n\tEmail = 6\n}"
  },
  {
    "path": "src/PopForums/Configuration/ICacheHelper.cs",
    "content": "namespace PopForums.Configuration;\n\npublic interface ICacheHelper\n{\n\t/// <summary>\n\t/// Saves an object to cache using he configured number of seconds.\n\t/// </summary>\n\t/// <param name=\"key\"></param>\n\t/// <param name=\"value\"></param>\n\tvoid SetCacheObject(string key, object value);\n\t/// <summary>\n\t/// Saves an object to cache using the specified number of seconds.\n\t/// </summary>\n\t/// <param name=\"key\"></param>\n\t/// <param name=\"value\"></param>\n\t/// <param name=\"seconds\"></param>\n\tvoid SetCacheObject(string key, object value, double seconds);\n\t/// <summary>\n\t/// Saves an object to cache without a time limit. Expiration should be deferred to \n\t/// the cache mechanism.\n\t/// </summary>\n\t/// <param name=\"key\"></param>\n\t/// <param name=\"value\"></param>\n\tvoid SetLongTermCacheObject(string key, object value);\n\t/// <summary>\n\t/// Stores an object in cache as a group. When the root key is removed, all of the paged items \n\t/// are also removed. Useful for storing paged threads and lists of topics.\n\t/// </summary>\n\t/// <typeparam name=\"T\">The type for the collections stored together (like a List&lt;Post&gt;).</typeparam>\n\t/// <param name=\"rootKey\">They key that references the collection of paged lists.</param>\n\t/// <param name=\"page\">The page number of the collection to store.</param>\n\t/// <param name=\"value\">The collection of T objects to cache.</param>\n\tvoid SetPagedListCacheObject<T>(string rootKey, int page, List<T> value);\n\t/// <summary>\n\t/// Removes an object from cache.\n\t/// </summary>\n\t/// <param name=\"key\"></param>\n\tvoid RemoveCacheObject(string key);\n\t/// <summary>\n\t/// Removes an object from cache.\n\t/// </summary>\n\t/// <param name=\"key\">Key of the cache item to remove.</param>\n\t/// <remarks>\n\t/// Implementations of this method should call the <see cref=\"OnRemoveCacheKey\"/> event.\n\t/// </remarks>\n\tT GetCacheObject<T>(string key);\n\n\tList<T> GetPagedListCacheObject<T>(string rootKey, int page);\n\t\n\t/// <summary>\n\t/// Can be used as a notification mechanism for downstream actions when cache is invalidated.\n\t/// </summary>\n\t/// <remarks>\n\t/// Implementations of ICacheHelper should call this event whenever they invalidate a cached object.\n\t/// </remarks>\n\tevent Action<string> OnRemoveCacheKey;\n\n\tstring GetEffectiveCacheKey(string key);\n}"
  },
  {
    "path": "src/PopForums/Configuration/Settings.cs",
    "content": "namespace PopForums.Configuration;\n\npublic class Settings\n{\n\tpublic Settings()\n\t{\n\t\tTermsOfService = string.Empty;\n\t\tIsNewUserApproved = true;\n\t\tTopicsPerPage = 20;\n\t\tPostsPerPage = 20;\n\t\tForumTitle = string.Empty;\n\t\tMinimumSecondsBetweenPosts = 30;\n\t\tSmtpServer = \"localhost\";\n\t\tSmtpPort = 25;\n\t\tMailerAddress = string.Empty;\n\t\tReplyToAddress = string.Empty;\n\t\tUseEsmtp = false;\n\t\tSmtpUser = string.Empty;\n\t\tSmtpPassword = string.Empty;\n\t\tMailSendingInverval = 1500;\n\t\tUseSslSmtp = false;\n\t\tSessionLength = 20;\n\t\tCensorWords = string.Empty;\n\t\tCensorCharacter = \"*\";\n\t\tAllowImages = false;\n\t\tLogSecurity = true;\n\t\tLogModeration = true;\n\t\tLogErrors = true;\n\t\tIsNewUserImageApproved = false;\n\t\tSearchIndexingInterval = 10000;\n\t\tIsSearchIndexingEnabled = true;\n\t\tIsMailerEnabled = true;\n\t\tUserImageMaxHeight = 300;\n\t\tUserImageMaxWidth = 400;\n\t\tUserImageMaxkBytes = 100;\n\t\tUserAvatarMaxHeight = 90;\n\t\tUserAvatarMaxWidth = 90;\n\t\tUserAvatarMaxkBytes = 10;\n\t\tMailSignature = string.Empty;\n\t\tScoringGameCalculatorInterval = 1000;\n\t\tMailerQuantity = 4;\n\t\tUseGoogleLogin = false;\n\t\tUseFacebookLogin = false;\n\t\tFacebookAppID = string.Empty;\n\t\tFacebookAppSecret = string.Empty;\n\t\tUseMicrosoftLogin = false;\n\t\tMicrosoftClientID = string.Empty;\n\t\tMicrosoftClientSecret = string.Empty;\n\t\tYouTubeHeight = 360;\n\t\tYouTubeWidth = 640;\n\t\tGoogleClientId = string.Empty;\n\t\tGoogleClientSecret = string.Empty;\n\t\tUseOAuth2Login = false;\n\t\tOAuth2ClientID = string.Empty;\n\t\tOAuth2ClientSecret = string.Empty;\n\t\tOAuth2LoginUrl = string.Empty;\n\t\tOAuth2TokenUrl = string.Empty;\n\t\tOAuth2DisplayName = string.Empty;\n\t\tOAuth2Scope = \"email\";\n\t\tIsClosingAgedTopics = false;\n\t\tCloseAgedTopicsDays = 365;\n\t\tIsPrivateForumInstance = false;\n\t\tPostImageMaxHeight = 1000;\n\t\tPostImageMaxWidth = 1000;\n\t\tPostImageMaxkBytes = 5000;\n\t}\n\n\tpublic virtual string TermsOfService { get; set; }\n\tpublic virtual bool IsNewUserApproved { get; set; }\n\tpublic virtual int TopicsPerPage { get; set; }\n\tpublic virtual int PostsPerPage { get; set; }\n\tpublic virtual string ForumTitle { get; set; }\n\tpublic virtual int MinimumSecondsBetweenPosts { get; set; }\n\tpublic virtual string SmtpServer { get; set; }\n\tpublic virtual int SmtpPort { get; set; }\n\tpublic virtual string MailerAddress { get; set; }\n\tpublic virtual string ReplyToAddress { get; set; }\n\tpublic virtual bool UseEsmtp { get; set; }\n\tpublic virtual string SmtpUser { get; set; }\n\tpublic virtual string SmtpPassword { get; set; }\n\tpublic virtual int MailSendingInverval { get; set; }\n\tpublic virtual bool UseSslSmtp { get; set; }\n\tpublic virtual int SessionLength { get; set; }\n\tpublic virtual string CensorWords { get; set; }\n\tpublic virtual string CensorCharacter { get; set; }\n\tpublic virtual bool AllowImages { get; set; }\n\tpublic virtual bool LogSecurity { get; set; }\n\tpublic virtual bool LogModeration { get; set; }\n\tpublic virtual bool LogErrors { get; set; }\n\tpublic virtual bool IsNewUserImageApproved { get; set; }\n\tpublic virtual int SearchIndexingInterval { get; set; }\n\tpublic virtual bool IsSearchIndexingEnabled { get; set; }\n\tpublic virtual bool IsMailerEnabled { get; set; }\n\tpublic virtual int UserImageMaxHeight { get; set; }\n\tpublic virtual int UserImageMaxWidth { get; set; }\n\tpublic virtual int UserImageMaxkBytes { get; set; }\n\tpublic virtual int UserAvatarMaxHeight { get; set; }\n\tpublic virtual int UserAvatarMaxWidth { get; set; }\n\tpublic virtual int UserAvatarMaxkBytes { get; set; }\n\tpublic virtual string MailSignature { get; set; }\n\tpublic virtual int ScoringGameCalculatorInterval { get; set; }\n\tpublic virtual int MailerQuantity { get; set; }\n\tpublic virtual bool UseGoogleLogin { get; set; }\n\tpublic virtual bool UseFacebookLogin { get; set; }\n\tpublic virtual string FacebookAppID { get; set; }\n\tpublic virtual string FacebookAppSecret { get; set; }\n\tpublic virtual bool UseMicrosoftLogin { get; set; }\n\tpublic virtual string MicrosoftClientID { get; set; }\n\tpublic virtual string MicrosoftClientSecret { get; set; }\n\tpublic virtual int YouTubeHeight { get; set; }\n\tpublic virtual int YouTubeWidth { get; set; }\n\tpublic virtual string GoogleClientId { get; set; }\n\tpublic virtual string GoogleClientSecret { get; set; }\n\tpublic virtual bool UseOAuth2Login { get; set; }\n\tpublic virtual string OAuth2ClientID { get; set; }\n\tpublic virtual string OAuth2ClientSecret { get; set; }\n\tpublic virtual string OAuth2LoginUrl { get; set; }\n\tpublic virtual string OAuth2TokenUrl { get; set; }\n\tpublic virtual string OAuth2DisplayName { get; set; }\n\tpublic virtual string OAuth2Scope { get; set; }\n\tpublic virtual bool IsClosingAgedTopics { get; set; }\n\tpublic virtual int CloseAgedTopicsDays { get; set; }\n\tpublic virtual bool IsPrivateForumInstance { get; set; }\n\tpublic virtual int PostImageMaxHeight { get; set; }\n\tpublic virtual int PostImageMaxWidth { get; set; }\n\tpublic virtual int PostImageMaxkBytes { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Configuration/SettingsManager.cs",
    "content": "﻿namespace PopForums.Configuration;\n\npublic interface ISettingsManager\n{\n\tSettings Current { get; }\n\tvoid SaveCurrent();\n\tvoid Save(Settings settings);\n}\n\npublic class SettingsManager : ISettingsManager\n{\n\tpublic SettingsManager(ISettingsRepository settingsRepository, IErrorLog errorLog)\n\t{\n\t\t_settingsRepository = settingsRepository;\n\t\t_errorLog = errorLog;\n\t\t_settingsRepository.OnSettingsInvalidated += () =>\n\t\t{\n\t\t\t_settings = null;\n\t\t};\n\t}\n\n\tprivate readonly ISettingsRepository _settingsRepository;\n\tprivate readonly IErrorLog _errorLog;\n\tprivate Settings _settings;\n\n\tpublic Settings Current\n\t{\n\t\tget\n\t\t{\n\t\t\tif (_settings == null)\n\t\t\t\tLoadSettings();\n\t\t\treturn _settings;\n\t\t}\n\t}\n\n\tprivate void LoadSettings()\n\t{\n\t\tvar dictionary = _settingsRepository.Get();\n\t\tvar settings = new Settings();\n\t\tforeach (var setting in dictionary.Keys)\n\t\t{\n\t\t\tvar property = settings.GetType().GetProperty(setting);\n\t\t\tif (property == null)\n\t\t\t{\n\t\t\t\t_errorLog.Log(null, ErrorSeverity.Warning, $\"Settings repository returned a setting called {setting}, which does not exist in code.\");\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tswitch (property.PropertyType.FullName)\n\t\t\t\t{\n\t\t\t\t\tcase \"System.Boolean\":\n\t\t\t\t\t\tproperty.SetValue(settings, Convert.ToBoolean(dictionary[setting]), null);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"System.String\":\n\t\t\t\t\t\tproperty.SetValue(settings, dictionary[setting], null);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"System.Int32\":\n\t\t\t\t\t\tproperty.SetValue(settings, Convert.ToInt32(dictionary[setting]), null);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"System.Double\":\n\t\t\t\t\t\tproperty.SetValue(settings, Convert.ToDouble(dictionary[setting]), null);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"System.DateTime\":\n\t\t\t\t\t\tproperty.SetValue(settings, Convert.ToDateTime(dictionary[setting]), null);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tthrow new Exception($\"Settings loader not coded to convert values of type {property.PropertyType.FullName}.\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_settings = settings;\n\t}\n\n\tpublic void Save(Settings settings)\n\t{\n\t\t_settings = settings;\n\t\tSaveCurrent();\n\t}\n\n\tpublic void SaveCurrent()\n\t{\n\t\tvar dictionary = new Dictionary<string, object>();\n\t\tvar properties = Current.GetType().GetProperties();\n\t\tforeach (var property in properties)\n\t\t{\n\t\t\tdictionary.Add(property.Name, property.GetValue(Current, null));\n\t\t}\n\t\t_settingsRepository.Save(dictionary);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Email/EmailQueuePayload.cs",
    "content": "﻿namespace PopForums.Email;\n\npublic class EmailQueuePayload\n{\n\tpublic int MessageID { get; set; }\n\tpublic EmailQueuePayloadType EmailQueuePayloadType { get; set; }\n\tpublic string ToEmail { get; set; }\n\tpublic string ToName { get; set; }\n\tpublic string TenantID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Email/EmailQueuePayloadType.cs",
    "content": "﻿namespace PopForums.Email;\n\npublic enum EmailQueuePayloadType\n{\n\tFullMessage = 1,\n\tMassMessage = 2,\n\tDeleteMassMessage = 3\n}"
  },
  {
    "path": "src/PopForums/Email/EmailWorker.cs",
    "content": "namespace PopForums.Email;\n\npublic interface IEmailWorker\n{\n\tTask Execute();\n}\n\npublic class EmailWorker(ISettingsManager settingsManager, ISmtpWrapper smtpWrapper, IQueuedEmailMessageRepository queuedEmailMessageRepository, IEmailQueueRepository emailQueueRepository, IErrorLog errorLog) : IEmailWorker\n{\n\tpublic async Task Execute()\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar messageGroup = new List<QueuedEmailMessage>();\n\t\t\tfor (var i = 1; i <= settingsManager.Current.MailerQuantity; i++)\n\t\t\t{\n\t\t\t\tvar payload = await emailQueueRepository.Dequeue();\n\t\t\t\tif (payload == null)\n\t\t\t\t\tbreak;\n\t\t\t\tif (payload.EmailQueuePayloadType == EmailQueuePayloadType.DeleteMassMessage)\n\t\t\t\t\tthrow new NotImplementedException($\"EmailQueuePayloadType {payload.EmailQueuePayloadType} not implemented.\");\n\t\t\t\tvar queuedMessage = await queuedEmailMessageRepository.GetMessage(payload.MessageID);\n\t\t\t\tif (payload.EmailQueuePayloadType == EmailQueuePayloadType.MassMessage)\n\t\t\t\t{\n\t\t\t\t\tqueuedMessage.ToEmail = payload.ToEmail;\n\t\t\t\t\tqueuedMessage.ToName = payload.ToName;\n\t\t\t\t}\n\t\t\t\tif (queuedMessage == null)\n\t\t\t\t\tbreak;\n\t\t\t\tmessageGroup.Add(queuedMessage);\n\t\t\t\tawait queuedEmailMessageRepository.DeleteMessage(queuedMessage.MessageID);\n\t\t\t}\n\t\t\tParallel.ForEach(messageGroup, message =>\n\t\t\t{\n\t\t\t\ttry\n\t\t\t\t{\n\t\t\t\t\tsmtpWrapper.Send(message);\n\t\t\t\t}\n\t\t\t\tcatch (Exception exc)\n\t\t\t\t{\n\t\t\t\t\tif (message == null)\n\t\t\t\t\t\terrorLog.Log(exc, ErrorSeverity.Email, \"There was no message for the MailWorker to send.\");\n\t\t\t\t\telse\n\t\t\t\t\t\terrorLog.Log(exc, ErrorSeverity.Email, $\"MessageID: {message.MessageID}, To: <{message.ToEmail}> {message.ToName}, Subject: {message.Subject}\");\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\terrorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/Email/ForgotPasswordMailer.cs",
    "content": "namespace PopForums.Email;\n\npublic interface IForgotPasswordMailer\n{\n\tTask ComposeAndQueue(User user, string resetLink);\n}\n\npublic class ForgotPasswordMailer : IForgotPasswordMailer\n{\n\tpublic ForgotPasswordMailer(ISettingsManager settingsManager, IQueuedEmailService queuedEmailService)\n\t{\n\t\t_settingsManager = settingsManager;\n\t\t_queuedEmailService = queuedEmailService;\n\t}\n\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IQueuedEmailService _queuedEmailService;\n\n\tpublic async Task ComposeAndQueue(User user, string resetLink)\n\t{\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(\"user\");\n\t\tif (String.IsNullOrEmpty(resetLink))\n\t\t\tthrow new ArgumentException(\"resetLink\");\n\t\tvar settings = _settingsManager.Current;\n\t\tvar body = String.Format(Resources.ForgotPasswordEmail\n\t\t\t, settings.ForumTitle, resetLink, settings.MailSignature, Environment.NewLine);\n\t\tvar message = new QueuedEmailMessage\n\t\t{\n\t\t\tBody = body,\n\t\t\tSubject = String.Format(Resources.ForgotPasswordSubject, settings.ForumTitle),\n\t\t\tToEmail = user.Email,\n\t\t\tToName = user.Name,\n\t\t\tFromName = settings.ForumTitle,\n\t\t\tQueueTime = DateTime.UtcNow\n\t\t};\n\t\tawait _queuedEmailService.CreateAndQueueEmail(message);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Email/MailingListComposer.cs",
    "content": "﻿namespace PopForums.Email;\n\npublic interface IMailingListComposer\n{\n\tvoid ComposeAndQueue(User user, string subject, string body, string htmlBody, string unsubscribeLink);\n}\n\npublic class MailingListComposer : IMailingListComposer\n{\n\tpublic MailingListComposer(ISettingsManager settingsManager, IQueuedEmailService queuedEmailService)\n\t{\n\t\t_settingsManager = settingsManager;\n\t\t_queuedEmailService = queuedEmailService;\n\t}\n\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IQueuedEmailService _queuedEmailService;\n\n\tpublic void ComposeAndQueue(User user, string subject, string body, string htmlBody, string unsubscribeLink)\n\t{\n\t\tvar settings = _settingsManager.Current;\n\t\tvar ps = $\"{Environment.NewLine}{Environment.NewLine}Unsubscribe: {unsubscribeLink}\";\n\t\tvar message = new QueuedEmailMessage\n\t\t{\n\t\t\tBody = body + ps,\n\t\t\tSubject = subject,\n\t\t\tToEmail = user.Email,\n\t\t\tToName = user.Name,\n\t\t\tFromName = settings.ForumTitle,\n\t\t\tQueueTime = DateTime.UtcNow\n\t\t};\n\t\tif (!string.IsNullOrWhiteSpace(htmlBody))\n\t\t\tmessage.HtmlBody = $\"{htmlBody}<p>Unsubscribe: <a href=\\\"{unsubscribeLink}\\\">{unsubscribeLink}</a></p>\";\n\t\t_queuedEmailService.CreateAndQueueEmail(message);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Email/NewAccountMailer.cs",
    "content": "﻿namespace PopForums.Email;\n\npublic interface INewAccountMailer\n{\n\tSmtpStatusCode? Send(User user, string verifyUrl);\n\n\t/// <summary>\n\t/// Used to deliver the text for a welcome e-mail, where the user is already \n\t/// approved. The default implementation uses Resources.RegisterEmailThankYou. \n\t/// It uses the following string format items:\n\t/// {0} Forum title (from settings)\n\t/// {1} Mail signature (from settings)\n\t/// {2} Environment.NewLine\n\t/// </summary>\n\tstring NewUserApprovedEmail { get; }\n\n\t/// <summary>\n\t/// Used to deliver the text for a welcome e-mail, where the user is must follow  \n\t/// a verification link. The default implementation uses Resources.RegisterEmailThankYou. \n\t/// It uses the following string format items:\n\t/// {0} Forum title (from settings)\n\t/// {1} Verification URL + auth code (generated by calling code)\n\t/// {2} Verification URL\n\t/// {3} Authorization key (from user object)\n\t/// {4} Mail signature (from settings)\n\t/// {5} Environment.NewLine\n\t/// </summary>\n\tstring NewUserVerifyEmail { get; }\n}\n\npublic class NewAccountMailer : INewAccountMailer\n{\n\tpublic NewAccountMailer(ISettingsManager settingsManager, ISmtpWrapper smtpWrapper)\n\t{\n\t\t_settingsManager = settingsManager;\n\t\t_smtpWrapper = smtpWrapper;\n\t}\n\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly ISmtpWrapper _smtpWrapper;\n\n\tpublic SmtpStatusCode? Send(User user, string verifyUrl)\n\t{\n\t\tvar settings = _settingsManager.Current;\n\t\tif (String.IsNullOrWhiteSpace(settings.MailerAddress))\n\t\t\tthrow new Exception(\"There is no MailerAddress to send e-mail from. Perhaps you didn't set up the settings.\");\n\t\tvar message = new EmailMessage\n\t\t{\n\t\t\tToEmail = user.Email,\n\t\t\tToName = user.Name,\n\t\t\tFromName = settings.ForumTitle\n\t\t};\n\t\tmessage.Subject = String.Format(Resources.RegisterEmailSubject, settings.ForumTitle);\n\t\tstring body;\n\t\tif (settings.IsNewUserApproved)\n\t\t\tbody = String.Format(NewUserApprovedEmail, settings.ForumTitle, settings.MailSignature, \"\\r\\n\");\n\t\telse\n\t\t\tbody = String.Format(NewUserVerifyEmail, settings.ForumTitle, verifyUrl + \"/\" + user.AuthorizationKey, verifyUrl, user.AuthorizationKey, settings.MailSignature, \"\\r\\n\");\n\t\tmessage.Body = body;\n\t\treturn _smtpWrapper.Send(message);\n\t}\n\n\tpublic virtual string NewUserApprovedEmail\n\t{\n\t\tget { return Resources.RegisterEmailThankYou; }\n\t}\n\n\tpublic virtual string NewUserVerifyEmail\n\t{\n\t\tget { return Resources.RegisterEmailThankYouVerify; }\n\t}\n}"
  },
  {
    "path": "src/PopForums/Email/SmtpStatusCode.cs",
    "content": "﻿namespace PopForums.Email;\n\npublic enum SmtpStatusCode\n{\n\tSystemStatus = 211,\n\tHelpMessage = 214,\n\tServiceReady = 220,\n\tServiceClosingTransmissionChannel = 221,\n\tAuthenticationSuccessful = 235,\n\tOk = 250,\n\tUserNotLocalWillForward = 251,\n\tCannotVerifyUserWillAttemptDelivery = 252,\n\tAuthenticationChallenge = 334,\n\tStartMailInput = 354,\n\tServiceNotAvailable = 421,\n\tPasswordTransitionNeeded = 432,\n\tMailboxBusy = 450,\n\tErrorInProcessing = 451,\n\tInsufficientStorage = 452,\n\tTemporaryAuthenticationFailure = 454,\n\tCommandUnrecognized = 500,\n\tSyntaxError = 501,\n\tCommandNotImplemented = 502,\n\tBadCommandSequence = 503,\n\tCommandParameterNotImplemented = 504,\n\tAuthenticationRequired = 530,\n\tAuthenticationMechanismTooWeak = 534,\n\tAuthenticationInvalidCredentials = 535,\n\tEncryptionRequiredForAuthenticationMechanism = 538,\n\tMailboxUnavailable = 550,\n\tUserNotLocalTryAlternatePath = 551,\n\tExceededStorageAllocation = 552,\n\tMailboxNameNotAllowed = 553,\n\tTransactionFailed = 554,\n\tMailFromOrRcptToParametersNotRecognizedOrNotImplemented = 555\n}"
  },
  {
    "path": "src/PopForums/Email/SmtpWrapper.cs",
    "content": "﻿using MailKit.Net.Smtp;\nusing MailKit.Security;\nusing MimeKit;\n\nnamespace PopForums.Email;\n\npublic interface ISmtpWrapper\n{\n\tSmtpStatusCode? Send(EmailMessage message);\n}\n\npublic class SmtpWrapper : ISmtpWrapper\n{\n\tpublic SmtpWrapper(ISettingsManager settingsManager, IErrorLog errorLog)\n\t{\n\t\t_settingsManager = settingsManager;\n\t\t_errorLog = errorLog;\n\t}\n\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IErrorLog _errorLog;\n\n\tpublic SmtpStatusCode? Send(EmailMessage message)\n\t{\n\t\tif (message == null)\n\t\t\tthrow new ArgumentNullException(nameof(message));\n\t\tvar parsedMessage = ConvertEmailMessage(message);\n\t\tvar settings = _settingsManager.Current;\n\t\tSmtpStatusCode? result = SmtpStatusCode.Ok;\n\t\tusing var client = new SmtpClient();\n\t\ttry\n\t\t{\n\t\t\tclient.Connect(settings.SmtpServer, settings.SmtpPort, settings.UseSslSmtp ? SecureSocketOptions.StartTls : SecureSocketOptions.None);\n\t\t\tif (settings.UseEsmtp)\n\t\t\t\tclient.Authenticate(settings.SmtpUser, settings.SmtpPassword);\n\t\t\tclient.Send(parsedMessage);\n\t\t}\n\t\tcatch (SmtpCommandException exc)\n\t\t{\n\t\t\tvar statusCode = (int)exc.StatusCode;\n\t\t\tresult = (SmtpStatusCode)statusCode;\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Email, $\"To: {message.ToEmail}, Subject: {message.Subject}, SmtpCommandException: {statusCode}\");\n\t\t}\n\t\tcatch (SmtpProtocolException exc)\n\t\t{\n\t\t\tresult = null;\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Email, $\"To: {message.ToEmail}, Subject: {message.Subject}, SmtpProtocolException: {exc.Message}\");\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\tresult = null;\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Email, $\"To: {message.ToEmail}, Subject: {message.Subject}, Exception: {exc.Message}\");\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\tclient.Disconnect(true);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate MimeMessage ConvertEmailMessage(EmailMessage forumMessage)\n\t{\n\t\tvar message = new MimeMessage();\n\t\tmessage.Headers.Add(\"X-Mailer\", \"POP Forums\");\n\t\tmessage.To.Add(new MailboxAddress(forumMessage.ToName, forumMessage.ToEmail));\n\t\tmessage.From.Add(new MailboxAddress(forumMessage.FromName, _settingsManager.Current.MailerAddress));\n\t\tif (!string.IsNullOrWhiteSpace(forumMessage.ReplyTo))\n\t\t\tmessage.ReplyTo.Add(new MailboxAddress(forumMessage.FromName, forumMessage.ReplyTo));\n\t\telse\n\t\t\tmessage.ReplyTo.Add(new MailboxAddress(forumMessage.FromName, _settingsManager.Current.ReplyToAddress));\n\t\tmessage.Subject = forumMessage.Subject;\n\t\tvar builder = new BodyBuilder();\n\t\tbuilder.TextBody = forumMessage.Body;\n\t\tbuilder.HtmlBody = forumMessage.HtmlBody;\n\t\tmessage.Body = builder.ToMessageBody();\n\t\treturn message;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Extensions/ServiceCollections.cs",
    "content": "﻿namespace PopForums.Extensions;\n\npublic static class ServiceCollections\n{\n\tpublic static void AddPopForumsBase(this IServiceCollection services)\n\t{\n\t\t// config\n\t\tservices.AddTransient<IConfig, Config>();\n\t\tservices.AddTransient<IErrorLog, ErrorLog>();\n\t\tservices.AddSingleton<ISettingsManager, SettingsManager>();\n\t\tservices.AddTransient<ITenantService, TenantService>();\n\n\t\t// composers\n\t\tservices.AddTransient<ITopicStateComposer, TopicStateComposer>();\n\t\tservices.AddTransient<IForumStateComposer, ForumStateComposer>();\n\t\tservices.AddTransient<IPrivateMessageStateComposer, PrivateMessageStateComposer>();\n\n\t\t// email\n\t\tservices.AddTransient<IForgotPasswordMailer, ForgotPasswordMailer>();\n\t\tservices.AddTransient<IMailingListComposer, MailingListComposer>();\n\t\tservices.AddTransient<INewAccountMailer, NewAccountMailer>();\n\t\tservices.AddTransient<ISmtpWrapper, SmtpWrapper>();\n\t\tservices.AddTransient<IEmailWorker, EmailWorker>();\n\n\t\t// external auth?\n\t\tservices.AddTransient<IExternalUserAssociationManager, ExternalUserAssociationManager>();\n\n\t\t// feeds\n\t\tservices.AddTransient<IFeedService, FeedService>();\n\n\t\t// scoring game\n\t\tservices.AddTransient<IAwardCalculator, AwardCalculator>();\n\t\tservices.AddTransient<IAwardDefinitionService, AwardDefinitionService>();\n\t\tservices.AddTransient<IEventDefinitionService, EventDefinitionService>();\n\t\tservices.AddTransient<IEventPublisher, EventPublisher>();\n\t\tservices.AddTransient<IUserAwardService, UserAwardService>();\n\n\t\t// services\n\t\tservices.AddTransient<IBanService, BanService>();\n\t\tservices.AddTransient<ICategoryService, CategoryService>();\n\t\tservices.AddTransient<IFavoriteTopicService, FavoriteTopicService>();\n\t\tservices.AddTransient<IForumService, ForumService>();\n\t\tservices.AddTransient<IImageService, ImageService>();\n\t\tservices.AddTransient<IIPHistoryService, IPHistoryService>();\n\t\tservices.AddTransient<ILastReadService, LastReadService>();\n\t\tservices.AddTransient<IMailingListService, MailingListService>();\n\t\tservices.AddTransient<IModerationLogService, ModerationLogService>();\n\t\tservices.AddTransient<IPostService, PostService>();\n\t\tservices.AddTransient<IPrivateMessageService, PrivateMessageService>();\n\t\tservices.AddTransient<IProfileService, ProfileService>();\n\t\tservices.AddTransient<IQueuedEmailService, QueuedEmailService>();\n\t\tservices.AddTransient<ISearchService, SearchService>();\n\t\tservices.AddTransient<ISecurityLogService, SecurityLogService>();\n\t\tservices.AddTransient<ISetupService, SetupService>();\n\t\tservices.AddTransient<ISubscribedTopicsService, SubscribedTopicsService>();\n\t\tservices.AddTransient<ITextParsingService, TextParsingService>();\n\t\tservices.AddTransient<ITopicService, TopicService>();\n\t\tservices.AddTransient<ITopicViewLogService, TopicViewLogService>();\n\t\tservices.AddTransient<IUserService, UserService>();\n\t\tservices.AddTransient<IUserSessionService, UserSessionService>();\n\t\tservices.AddTransient<IServiceHeartbeatService, ServiceHeartbeatService>();\n\t\tservices.AddTransient<ISearchIndexSubsystem, SearchIndexSubsystem>();\n\t\tservices.AddTransient<IPostMasterService, PostMasterService>();\n\t\tservices.AddTransient<IForumPermissionService, ForumPermissionService>();\n\t\tservices.AddTransient<IReCaptchaService, ReCaptchaService>();\n\t\tservices.AddTransient<ISitemapService, SitemapService>();\n\t\tservices.AddTransient<ITimeFormatStringService, TimeFormatStringService>();\n\t\tservices.AddTransient<IResourceComposer, ResourceComposer>();\n\t\tservices.AddTransient<INotificationManager, NotificationManager>();\n\t\tservices.AddTransient<INotificationAdapter, NotificationAdapter>();\n\t\tservices.AddTransient<INotificationTunnel, NotificationTunnel>();\n\t\tservices.AddTransient<IPostImageService, PostImageService>();\n\t\tservices.AddTransient<IClaimsToRoleMapper, ClaimsToRoleMapper>();\n\t\tservices.AddTransient<IUserNameReconciler, UserNameReconciler>();\n\t\tservices.AddTransient<IUserEmailReconciler, UserEmailReconciler>();\n\t\tservices.AddTransient<IUserSessionWorker, UserSessionWorker>();\n\t\tservices.AddTransient<ISearchIndexWorker, SearchIndexWorker>();\n\t\tservices.AddTransient<IAwardCalculatorWorker, AwardCalculatorWorker>();\n\t\tservices.AddTransient<ICloseAgedTopicsWorker, CloseAgedTopicsWorker>();\n\t\tservices.AddTransient<IPostImageCleanupWorker, PostImageCleanupWorker>();\n\t\tservices.AddTransient<ISubscribeNotificationWorker, SubscribeNotificationWorker>();\n\t\tservices.AddTransient<IIgnoreService, IgnoreService>();\n\t}\n}"
  },
  {
    "path": "src/PopForums/Extensions/Streams.cs",
    "content": "﻿namespace PopForums.Extensions;\n\npublic static class Streams\n{\n\tpublic static byte[] ToBytes(this Stream stream)\n\t{\n\t\tvar length = (int)stream.Length;\n\t\tvar bytes = new byte[length];\n\t\tstream.ReadExactly(bytes, 0, length);\n\t\treturn bytes;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Extensions/Strings.cs",
    "content": "﻿namespace PopForums.Extensions;\n\npublic static class Strings\n{\n\tpublic static string GetSHA256Hash(this string text)\n\t{\n\t\tif (string.IsNullOrWhiteSpace(text))\n\t\t{\n\t\t\treturn string.Empty;\n\t\t}\n\t\tvar input = Encoding.UTF8.GetBytes(text);\n\t\tusing (var sha256 = SHA256.Create())\n\t\t{\n\t\t\tvar output = sha256.ComputeHash(input);\n\t\t\treturn Convert.ToBase64String(output);\n\t\t}\n\t}\n\n\tpublic static string GetSHA256Hash(this string text, Guid salt)\n\t{\n\t\tvar concatString = text + salt;\n\t\treturn GetSHA256Hash(concatString);\n\t}\n\n\tpublic static string GetMD5Hash(this string text)\n\t{\n\t\tif (string.IsNullOrWhiteSpace(text))\n\t\t{\n\t\t\treturn string.Empty;\n\t\t}\n\t\tvar input = Encoding.UTF8.GetBytes(text);\n\t\tusing (var md5 = MD5.Create())\n\t\t{\n\t\t\tvar output = md5.ComputeHash(input);\n\t\t\treturn Convert.ToBase64String(output);\n\t\t}\n\t}\n\n\tpublic static string GetMD5Hash(this string text, Guid salt)\n\t{\n\t\tvar concatString = text + salt;\n\t\treturn GetMD5Hash(concatString);\n\t}\n\n\tpublic static bool IsEmailAddress(this string text)\n\t{\n\t\treturn Regex.IsMatch(text, @\"^\\S+?@([a-z0-9\\-\\.])+?\\.([a-z0-9\\-\\.])+$\", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);\n\t}\n\n\tpublic static string ToUrlName(this string text)\n\t{\n\t\tif (text == null)\n\t\t\tthrow new Exception(\"Can't Url convert a null string.\");\n\t\tvar result = text.Replace(\" \", \"-\");\n\t\tvar replacer = new Regex(@\"[^\\w\\-]\", RegexOptions.None);\n\t\tresult = replacer.Replace(result, \"\").ToLower();\n\t\treturn result;\n\t}\n\n\tpublic static string ToUniqueUrlName(this string name, List<string> matchingStartsWith)\n\t{\n\t\tvar urlName = name.ToUrlName();\n\t\treturn ToUniqueName(urlName, matchingStartsWith);\n\t}\n\n\tpublic static string ToUniqueName(this string name, List<string> matchingStartsWith)\n\t{\n\t\tname = Regex.Escape(name).Replace(\"\\\\ \", \" \");\n\t\tvar originalName = name;\n\t\tvar matchTest = name.Replace(\"-\", @\"\\-\");\n\t\tvar count = matchingStartsWith.Count(m => Regex.IsMatch(m, @\"^(\" + matchTest + @\")(\\-\\d)?$\"));\n\t\tif (count > 0)\n\t\t\tname = name + \"-\" + (count + 1);\n\t\twhile (matchingStartsWith.Exists(x => x == name))\n\t\t{\n\t\t\tcount++;\n\t\t\tname = originalName + \"-\" + (count + 1);\n\t\t}\n\n\t\treturn name;\n\t}\n\n\tpublic static string Trimmer(this string stringToTrim, int maxLength)\n\t{\n\t\tif (maxLength < 20) maxLength = 20;\n\t\tif (stringToTrim.Length <= maxLength) return stringToTrim;\n\t\treturn stringToTrim.Substring(0, maxLength - 13) + \"...\" +\n\t\t       stringToTrim.Substring(stringToTrim.Length - 10, 10);\n\t}\n}\n"
  },
  {
    "path": "src/PopForums/Extensions/Users.cs",
    "content": "﻿namespace PopForums.Extensions;\n\npublic static class Users\n{\n\tpublic static bool IsPostEditable(this User user, Post post)\n\t{\n\t\tif (user == null)\n\t\t\treturn false;\n\t\treturn user.IsInRole(PermanentRoles.Moderator) || user.UserID == post.UserID;\n\t}\n}"
  },
  {
    "path": "src/PopForums/ExternalLogin/ExternalAuthenticationResult.cs",
    "content": "﻿namespace PopForums.ExternalLogin;\n\npublic class ExternalAuthenticationResult\n{\n\tpublic string Issuer { get; set; }\n\tpublic string ProviderKey { get; set; }\n\tpublic string Name { get; set; }\n\tpublic string Email { get; set; }\n}"
  },
  {
    "path": "src/PopForums/ExternalLogin/ExternalLoginInfo.cs",
    "content": "﻿namespace PopForums.ExternalLogin;\n\npublic class ExternalLoginInfo\n{\n\tpublic ExternalLoginInfo(string loginProvider, string providerKey,\n\t\tstring displayName)\n\t{\n\t\tLoginProvider = loginProvider;\n\t\tProviderKey = providerKey;\n\t\tProviderDisplayName = displayName;\n\t}\n\n\tpublic string LoginProvider { get; set; }\n\tpublic string ProviderKey { get; set; }\n\tpublic string ProviderDisplayName { get; set; }\n}"
  },
  {
    "path": "src/PopForums/ExternalLogin/ExternalUserAssociation.cs",
    "content": "﻿namespace PopForums.ExternalLogin;\n\npublic class ExternalUserAssociation\n{\n\tpublic int ExternalUserAssociationID { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string Issuer { get; set; }\n\tpublic string ProviderKey { get; set; }\n\tpublic string Name { get; set; }\n}"
  },
  {
    "path": "src/PopForums/ExternalLogin/ExternalUserAssociationManager.cs",
    "content": "﻿namespace PopForums.ExternalLogin;\n\npublic interface IExternalUserAssociationManager\n{\n\tTask<ExternalUserAssociationMatchResult> ExternalUserAssociationCheck(ExternalLoginInfo externalLoginInfo, string ip);\n\tTask Associate(User user, ExternalLoginInfo externalLoginInfo, string ip);\n\tTask<List<ExternalUserAssociation>> GetExternalUserAssociations(User user);\n\tTask RemoveAssociation(User user, int externalUserAssociationID, string ip);\n}\n\npublic class ExternalUserAssociationManager : IExternalUserAssociationManager\n{\n\tpublic ExternalUserAssociationManager(IExternalUserAssociationRepository externalUserAssociationRepository, IUserRepository userRepository, ISecurityLogService securityLogService)\n\t{\n\t\t_externalUserAssociationRepository = externalUserAssociationRepository;\n\t\t_userRepository = userRepository;\n\t\t_securityLogService = securityLogService;\n\t}\n\n\tprivate readonly IExternalUserAssociationRepository _externalUserAssociationRepository;\n\tprivate readonly IUserRepository _userRepository;\n\tprivate readonly ISecurityLogService _securityLogService;\n\n\tpublic async Task<ExternalUserAssociationMatchResult> ExternalUserAssociationCheck(ExternalLoginInfo externalLoginInfo, string ip)\n\t{\n\t\tif (externalLoginInfo == null)\n\t\t\tthrow new ArgumentNullException(nameof(externalLoginInfo));\n\t\tvar match = await _externalUserAssociationRepository.Get(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey);\n\t\tif (match == null)\n\t\t{\n\t\t\tawait _securityLogService.CreateLogEntry((int?)null, null, ip, $\"Issuer: {externalLoginInfo.LoginProvider}, Provider: {externalLoginInfo.ProviderKey}, Name: {externalLoginInfo.ProviderDisplayName}\", SecurityLogType.ExternalAssociationCheckFailed);\n\t\t\treturn new ExternalUserAssociationMatchResult {Successful = false};\n\t\t}\n\t\tvar user = await _userRepository.GetUser(match.UserID);\n\t\tif (user == null)\n\t\t{\n\t\t\tawait _securityLogService.CreateLogEntry((int?)null, null, ip, $\"Issuer: {externalLoginInfo.LoginProvider}, Provider: {externalLoginInfo.ProviderKey}, Name: {externalLoginInfo.ProviderDisplayName}\", SecurityLogType.ExternalAssociationCheckFailed);\n\t\t\treturn new ExternalUserAssociationMatchResult {Successful = false};\n\t\t}\n\t\tvar result = new ExternalUserAssociationMatchResult\n\t\t{\n\t\t\tSuccessful = true,\n\t\t\tExternalUserAssociation = match,\n\t\t\tUser = user\n\t\t};\n\t\tawait _securityLogService.CreateLogEntry(user, user, ip, $\"Issuer: {match.Issuer}, Provider: {match.ProviderKey}, Name: {match.Name}\", SecurityLogType.ExternalAssociationCheckSuccessful);\n\t\treturn result;\n\t}\n\n\tpublic async Task Associate(User user, ExternalLoginInfo externalLoginInfo, string ip)\n\t{\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(nameof(user));\n\t\tif (externalLoginInfo != null)\n\t\t{\n\t\t\tif (string.IsNullOrEmpty(externalLoginInfo.LoginProvider))\n\t\t\t\tthrow new NullReferenceException(\"The external login info contains no provider.\");\n\t\t\tif (string.IsNullOrEmpty(externalLoginInfo.ProviderKey))\n\t\t\t\tthrow new NullReferenceException(\"The external login info contains no provider key.\");\n\t\t\tif (string.IsNullOrEmpty(externalLoginInfo.ProviderDisplayName))\n\t\t\t\texternalLoginInfo.ProviderDisplayName = string.Empty;\n\t\t\tawait _externalUserAssociationRepository.Save(user.UserID, externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey, externalLoginInfo.ProviderDisplayName);\n\t\t\tawait _securityLogService.CreateLogEntry(user, user, ip, $\"Provider: {externalLoginInfo.LoginProvider}, DisplayName: {externalLoginInfo.ProviderDisplayName}\", SecurityLogType.ExternalAssociationSet);\n\t\t}\n\t}\n\n\tpublic async Task<List<ExternalUserAssociation>> GetExternalUserAssociations(User user)\n\t{\n\t\treturn await _externalUserAssociationRepository.GetByUser(user.UserID);\n\t}\n\n\tpublic async Task RemoveAssociation(User user, int externalUserAssociationID, string ip)\n\t{\n\t\tvar association = await _externalUserAssociationRepository.Get(externalUserAssociationID);\n\t\tif (association == null)\n\t\t\treturn;\n\t\tif (association.UserID != user.UserID)\n\t\t\tthrow new Exception($\"Can't delete external user association {externalUserAssociationID} because it doesn't match UserID {user.UserID}.\");\n\t\tawait _externalUserAssociationRepository.Delete(externalUserAssociationID);\n\t\tawait _securityLogService.CreateLogEntry(user, user, ip, $\"Issuer: {association.Issuer}, Provider: {association.ProviderKey}, Name: {association.Name}\", SecurityLogType.ExternalAssociationRemoved);\n\t}\n}"
  },
  {
    "path": "src/PopForums/ExternalLogin/ExternalUserAssociationMatchResult.cs",
    "content": "﻿namespace PopForums.ExternalLogin;\n\npublic class ExternalUserAssociationMatchResult\n{\n\tpublic bool Successful { get; set; }\n\tpublic ExternalUserAssociation ExternalUserAssociation { get; set; }\n\tpublic User User { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Feeds/FeedService.cs",
    "content": "namespace PopForums.Feeds;\n\npublic interface IFeedService\n{\n\tTask PublishToFeed(User user, string message, int points, DateTime timeStamp);\n\tTask<List<FeedEvent>> GetFeed(User user);\n}\n\npublic class FeedService : IFeedService\n{\n\n\tpublic FeedService(IFeedRepository feedRepository, IBroker broker)\n\t{\n\t\t_feedRepository = feedRepository;\n\t\t_broker = broker;\n\t}\n\n\tprivate readonly IFeedRepository _feedRepository;\n\tprivate readonly IBroker _broker;\n\t\t\n\tpublic const int MaxFeedCount = 50;\n\n\tpublic async Task PublishToFeed(User user, string message, int points, DateTime timeStamp)\n\t{\n\t\tif (user == null)\n\t\t\treturn;\n\t\tawait _feedRepository.PublishEvent(user.UserID, message, points, timeStamp);\n\t\tvar cutOff = await _feedRepository.GetOldestTime(user.UserID, MaxFeedCount);\n\t\tawait _feedRepository.DeleteOlderThan(user.UserID, cutOff);\n\t}\n\n\tpublic async Task<List<FeedEvent>> GetFeed(User user)\n\t{\n\t\treturn await _feedRepository.GetFeed(user.UserID, MaxFeedCount);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Global.cs",
    "content": "﻿global using System;\nglobal using System.Collections;\nglobal using System.Collections.Generic;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Security;\nglobal using System.Security.Cryptography;\nglobal using System.Text;\nglobal using System.Text.Encodings.Web;\nglobal using System.Text.Json;\nglobal using System.Text.Json.Serialization;\nglobal using System.Text.RegularExpressions;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\n\nglobal using Microsoft.Extensions.Configuration;\nglobal using Microsoft.Extensions.DependencyInjection;\n\nglobal using PopForums.Composers;\nglobal using PopForums.Configuration;\nglobal using PopForums.Email;\nglobal using PopForums.Extensions;\nglobal using PopForums.ExternalLogin;\nglobal using PopForums.Feeds;\nglobal using PopForums.Messaging;\nglobal using PopForums.Models;\nglobal using PopForums.Repositories;\nglobal using PopForums.ScoringGame;\nglobal using PopForums.Services;"
  },
  {
    "path": "src/PopForums/Messaging/IBroker.cs",
    "content": "﻿namespace PopForums.Messaging;\n\npublic interface IBroker\n{\n\tvoid NotifyNewPosts(Topic topic, int lasPostID);\n\tvoid NotifyForumUpdate(Forum forum);\n\tvoid NotifyTopicUpdate(Topic topic, Forum forum, string topicLink);\n\tvoid NotifyNewPost(Topic topic, int postID);\n\tvoid NotifyPMCount(int userID, int pmCount);\n\tvoid NotifyUser(Notification notification);\n\tvoid NotifyUser(Notification notification, string tenantID);\n\tvoid SendPMMessage(PrivateMessagePost post);\n}"
  },
  {
    "path": "src/PopForums/Messaging/Models/AwardData.cs",
    "content": "﻿namespace PopForums.Messaging.Models;\n\npublic class AwardData\n{\n\tpublic string Title { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Messaging/Models/AwardPayload.cs",
    "content": "﻿namespace PopForums.Messaging.Models;\n\npublic class AwardPayload\n{\n\tpublic string Title { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string TenantID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Messaging/Models/QuestionData.cs",
    "content": "﻿namespace PopForums.Messaging.Models;\n\npublic class QuestionData\n{\n\tpublic string AskerName { get; set; }\n\tpublic string Title { get; set; }\n\tpublic int PostID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Messaging/Models/ReplyData.cs",
    "content": "﻿namespace PopForums.Messaging.Models;\n\npublic class ReplyData\n{\n\tpublic string PostName { get; set; }\n\tpublic int TopicID { get; set; }\n\tpublic string Title { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Messaging/Models/ReplyPayload.cs",
    "content": "﻿namespace PopForums.Messaging.Models;\n\npublic class ReplyPayload\n{\n\tpublic string PostName { get; set; }\n\tpublic string Title { get; set; }\n\tpublic int TopicID { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string TenantID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Messaging/Models/VoteData.cs",
    "content": "﻿namespace PopForums.Messaging.Models;\n\npublic class VoteData\n{\n\tpublic string VoterName { get; set; }\n\tpublic string Title { get; set; }\n\tpublic int PostID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Messaging/Notification.cs",
    "content": "﻿namespace PopForums.Messaging;\n\npublic class Notification\n{\n\tpublic int UserID { get; set; }\n\tpublic DateTime TimeStamp { get; set; }\n\tpublic bool IsRead { get; set; }\n\tpublic NotificationType NotificationType { get; set; }\n\tpublic long ContextID { get; set; }\n\tpublic JsonElement Data { get; set; }\n\tpublic int UnreadCount { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Messaging/NotificationAdapter.cs",
    "content": "﻿using PopForums.Messaging.Models;\n\nnamespace PopForums.Messaging;\n\npublic interface INotificationAdapter\n{\n\tTask Reply(string postName, string title, int topicID, int userID, string tenantID);\n\tTask Vote(string voterName, string title, int postID, int userID);\n\tTask QuestionAnswer(string askerName, string title, int postID, int userID);\n\tTask Award(string title, int userID);\n\tTask Award(string title, int userID, string tenantID);\n}\n\npublic class NotificationAdapter : INotificationAdapter\n{\n\tprivate readonly INotificationManager _notificationManager;\n\n\tpublic NotificationAdapter(INotificationManager notificationManager)\n\t{\n\t\t_notificationManager = notificationManager;\n\t}\n\n\tpublic async Task Reply(string postName, string title, int topicID, int userID, string tenantID)\n\t{\n\t\tvar replyData = new ReplyData\n\t\t{\n\t\t\tPostName = postName,\n\t\t\tTitle = title,\n\t\t\tTopicID = topicID\n\t\t};\n\t\tawait _notificationManager.ProcessNotification(userID, NotificationType.NewReply, replyData.TopicID, replyData, tenantID);\n\t}\n\n\tpublic async Task Vote(string voterName, string title, int postID, int userID)\n\t{\n\t\tvar voteData = new VoteData\n\t\t{\n\t\t\tVoterName = voterName,\n\t\t\tTitle = title,\n\t\t\tPostID = postID\n\t\t};\n\t\tawait _notificationManager.ProcessNotification(userID, NotificationType.VoteUp, postID, voteData);\n\t}\n\n\tpublic async Task QuestionAnswer(string askerName, string title, int postID, int userID)\n\t{\n\t\tvar questionData = new QuestionData\n\t\t{\n\t\t\tAskerName = askerName,\n\t\t\tTitle = title,\n\t\t\tPostID = postID\n\t\t};\n\t\tawait _notificationManager.ProcessNotification(userID, NotificationType.QuestionAnswered, postID, questionData);\n\t}\n\n\tpublic async Task Award(string title, int userID)\n\t{\n\t\tawait Award(title, userID, null);\n\t}\n\n\tpublic async Task Award(string title, int userID, string tenantID)\n\t{\n\t\tvar awardData = new AwardData\n\t\t{\n\t\t\tTitle = title\n\t\t};\n\t\tvar sequentialContext = DateTime.UtcNow.Ticks;\n\t\tawait _notificationManager.ProcessNotification(userID, NotificationType.Award, sequentialContext, awardData, tenantID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Messaging/NotificationManager.cs",
    "content": "﻿namespace PopForums.Messaging;\n\npublic interface INotificationManager\n{\n\tTask MarkNotificationRead(int userID, NotificationType notificationType, long contextID);\n\tTask ProcessNotification(int userID, NotificationType notificationType, long contextID, dynamic data);\n\tTask ProcessNotification(int userID, NotificationType notificationType, long contextID, dynamic data, string tenantID);\n\tTask<int> GetUnreadNotificationCount(int userID);\n\tTask MarkAllRead(int userID);\n\tTask<List<Notification>> GetNotifications(int userID, DateTime afterDateTime);\n}\n\npublic class NotificationManager : INotificationManager\n{\n\tprivate readonly INotificationRepository _notificationRepository;\n\tprivate readonly IBroker _broker;\n\tprivate const int PageSize = 20;\n\tprivate const int MaxNotificationCount = 100;\n\n\tpublic NotificationManager(INotificationRepository notificationRepository, IBroker broker)\n\t{\n\t\t_notificationRepository = notificationRepository;\n\t\t_broker = broker;\n\t}\n\n\tpublic async Task ProcessNotification(int userID, NotificationType notificationType, long contextID, dynamic data)\n\t{\n\t\tawait ProcessNotification(userID, notificationType, contextID, data, null);\n\t}\n\n\tpublic async Task ProcessNotification(int userID, NotificationType notificationType, long contextID, dynamic data, string tenantID)\n\t{\n\t\tvar serializedData = JsonSerializer.SerializeToElement(data, new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n\t\tvar notification = new Notification\n\t\t{\n\t\t\tUserID = userID,\n\t\t\tTimeStamp = DateTime.UtcNow,\n\t\t\tIsRead = false,\n\t\t\tNotificationType = notificationType,\n\t\t\tContextID = contextID,\n\t\t\tData = serializedData\n\t\t};\n\t\t\n\t\tvar recordsUpdated = await _notificationRepository.UpdateNotification(notification);\n\t\tif (recordsUpdated == 0)\n\t\t\tawait _notificationRepository.CreateNotification(notification);\n\t\tnotification.UnreadCount = await _notificationRepository.GetUnreadNotificationCount(userID);\n\n\t\tif (tenantID == null || string.IsNullOrWhiteSpace(tenantID))\n\t\t\t_broker.NotifyUser(notification);\n\t\telse\n\t\t\t_broker.NotifyUser(notification, tenantID);\n\t}\n\n\tpublic async Task MarkNotificationRead(int userID, NotificationType notificationType, long contextID)\n\t{\n\t\tawait _notificationRepository.MarkNotificationRead(userID, notificationType, contextID);\n\t}\n\n\tpublic async Task<List<Notification>> GetNotifications(int userID, DateTime afterDateTime)\n\t{\n\t\treturn await _notificationRepository.GetNotifications(userID, afterDateTime, PageSize);\n\t}\n\n\tpublic async Task<int> GetUnreadNotificationCount(int userID)\n\t{\n\t\tvar count = await _notificationRepository.GetUnreadNotificationCount(userID);\n\t\treturn count > MaxNotificationCount ? MaxNotificationCount : count;\n\t}\n\n\tpublic async Task MarkAllRead(int userID)\n\t{\n\t\tawait _notificationRepository.MarkAllRead(userID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Messaging/NotificationTunnel.cs",
    "content": "﻿namespace PopForums.Messaging;\n\npublic interface INotificationTunnel\n{\n\tvoid SendNotificationForUserAward(string title, int userID, string tenantID);\n\tvoid SendNotificationForReply(string postName, string title, int topicID, int userID, string tenantID);\n}\n\npublic class NotificationTunnel : INotificationTunnel\n{\n\tpublic static string HeaderName = \"PfApi\";\n\n\tprivate readonly INotificationAdapter _notificationAdapter;\n\n\tpublic NotificationTunnel(INotificationAdapter notificationAdapter)\n\t{\n\t\t_notificationAdapter = notificationAdapter;\n\t}\n\n\tpublic async void SendNotificationForUserAward(string title, int userID, string tenantID)\n\t{\n\t\tawait _notificationAdapter.Award(title, userID, tenantID);\n\t}\n\n\tpublic async void SendNotificationForReply(string postName, string title, int topicID, int userID, string tenantID)\n\t{\n\t\tawait _notificationAdapter.Reply(postName, title, topicID, userID, tenantID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Messaging/NotificationType.cs",
    "content": "﻿namespace PopForums.Messaging;\n\npublic enum NotificationType\n{\n\tNewReply = 0,\n\tVoteUp = 1,\n\tQuestionAnswered = 2,\n\tAward = 3\n}"
  },
  {
    "path": "src/PopForums/Models/AwardCalculationPayload.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class AwardCalculationPayload\n{\n\tpublic string EventDefinitionID { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string TenantID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/BasicJsonMessage.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class BasicJsonMessage\n{\n\tpublic bool Result { get; set; }\n\tpublic string Message { get; set; }\n\tpublic object Data { get; set; }\n\tpublic string Redirect { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/BasicServiceResponse.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class BasicServiceResponse<T> where T : class\n{\n\tpublic bool IsSuccessful { get; set; }\n\tpublic string Message { get; set; }\n\tpublic T Data { get; set; }\n\tpublic string Redirect { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/CategorizedForumContainer.cs",
    "content": "namespace PopForums.Models;\n\npublic class CategorizedForumContainer\n{\n\tpublic CategorizedForumContainer(IEnumerable<Category> categories, IEnumerable<Forum> forums)\n\t{\n\t\tReadStatusLookup = new Dictionary<int, ReadStatus>();\n\t\tAllCategories = categories;\n\t\tAllForums = forums;\n\t\tUncategorizedForums = forums.Where(f => !f.CategoryID.HasValue || f.CategoryID == 0).OrderBy(f => f.SortOrder).ToList();\n\t\tCategoryDictionary = new Dictionary<Category, List<Forum>>();\n\t\tforeach (var category in AllCategories.OrderBy(c => c.SortOrder))\n\t\t{\n\t\t\tvar forumSet = AllForums.Where(f => f.CategoryID == category.CategoryID).OrderBy(f => f.SortOrder).ToList();\n\t\t\tif (forumSet.Count > 0)\n\t\t\t\tCategoryDictionary.Add(category, forumSet);\n\t\t}\n\t}\n\n\tpublic IEnumerable<Category> AllCategories { get; private set; }\n\tpublic IEnumerable<Forum> AllForums { get; private set; }\n\tpublic List<Forum> UncategorizedForums { get; private set; }\n\tpublic Dictionary<Category, List<Forum>> CategoryDictionary { get; private set; }\n\tpublic string ForumTitle { get; set; }\n\tpublic Dictionary<int, ReadStatus> ReadStatusLookup { get; private set; }\n}"
  },
  {
    "path": "src/PopForums/Models/Category.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class Category\n{\n\tpublic int CategoryID { get; set; }\n\tpublic string Title { get; set; }\n\tpublic int SortOrder { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/CategoryContainerWithForums.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class CategoryContainerWithForums\n{\n\tpublic Category Category { get; set; }\n\tpublic IEnumerable<Forum> Forums { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ClientPrivateMessagePost.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class ClientPrivateMessagePost\n{\n\tpublic int PMPostID { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string Name { get; set; }\n\tpublic string PostTime { get; set; }\n\tpublic string FullText { get; set; }\n\n\tpublic static ClientPrivateMessagePost[] MapForClient(List<PrivateMessagePost> posts)\n\t{\n\t\tvar messages = posts.Select(x => new ClientPrivateMessagePost { PMPostID = x.PMPostID, UserID = x.UserID, Name = x.Name, PostTime = x.PostTime.ToString(\"o\"), FullText = x.FullText }).ToArray();\n\t\treturn messages;\n\t}\n\n\tpublic static ClientPrivateMessagePost MapForClient(PrivateMessagePost post)\n\t{\n\t\tvar message = new ClientPrivateMessagePost { PMPostID = post.PMPostID, UserID = post.UserID, Name = post.Name, PostTime = post.PostTime.ToString(\"o\"), FullText = post.FullText };\n\t\treturn message;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Models/DisplayProfile.cs",
    "content": "namespace PopForums.Models;\n\npublic class DisplayProfile\n{\n\tpublic DisplayProfile(User user, Profile profile, UserImage userImage)\n\t{\n\t\tUserID = user.UserID;\n\t\tName = user.Name;\n\t\tJoined = user.CreationDate;\n\t\tDob = profile.Dob;\n\t\tLocation = profile.Location;\n\t\tWeb = profile.Web;\n\t\tInstagram = profile.Instagram;\n\t\tFacebook = profile.Facebook;\n\t\tAvatarID = profile.AvatarID;\n\t\tImageID = profile.ImageID;\n\t\tShowDetails = profile.ShowDetails;\n\t\tif (userImage != null && userImage.IsApproved)\n\t\t\tIsImageApproved = true;\n\t\tPoints = profile.Points;\n\t\tIsApproved = user.IsApproved;\n\t}\n\n\tpublic int UserID { get; set; }\n\tpublic string Name { get; set; }\n\tpublic DateTime Joined { get; set; }\n\tpublic DateTime? Dob { get; set; }\n\tpublic string Location { get; set; }\n\tpublic int PostCount { get; set; }\n\tpublic string Web { get; set; }\n\tpublic string Instagram { get; set; }\n\tpublic string Facebook { get; set; }\n\tpublic int? AvatarID { get; set; }\n\tpublic int? ImageID { get; set; }\n\tpublic bool ShowDetails { get; set; }\n\tpublic bool IsImageApproved { get; set; }\n\tpublic int Points { get; set; }\n\tpublic List<FeedEvent> Feed { get; set; }\n\tpublic List<UserAward> UserAwards { get; set; }\n\tpublic bool IsApproved { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/EmailMessage.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class EmailMessage\n{\n\tpublic string FromName { get; set; }\n\tpublic string ToEmail { get; set; }\n\tpublic string ToName { get; set; }\n\tpublic string Subject { get; set; }\n\tpublic string Body { get; set; }\n\tpublic string HtmlBody { get; set; }\n\tpublic string ReplyTo { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ErrorLogEntry.cs",
    "content": "namespace PopForums.Models;\n\npublic class ErrorLogEntry\n{\n\tpublic int ErrorID { get; set; }\n\tpublic DateTime TimeStamp { get; set; }\n\tpublic string Message { get; set; }\n\tpublic string StackTrace { get; set; }\n\tpublic string Data { get; set; }\n\tpublic ErrorSeverity Severity { get; set; }\n\tpublic string SeverityString => Severity.ToString();\n}"
  },
  {
    "path": "src/PopForums/Models/ExpiredUserSession.cs",
    "content": "namespace PopForums.Models;\n\npublic class ExpiredUserSession\n{\n\tpublic int SessionID { get; set; }\n\tpublic int? UserID { get; set; }\n\tpublic DateTime LastTime { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/FeedEvent.cs",
    "content": "namespace PopForums.Models;\n\npublic class FeedEvent\n{\n\tpublic int UserID { get; set; }\n\tpublic string Message { get; set; }\n\tpublic int Points { get; set; }\n\tpublic DateTime TimeStamp { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/Forum.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class Forum\n{\n\tpublic int ForumID { get; set; }\n\tpublic int? CategoryID { get; set; }\n\tpublic string Title { get; set; }\n\tpublic string Description { get; set; }\n\tpublic bool IsVisible { get; set; }\n\tpublic bool IsArchived { get; set; }\n\tpublic int SortOrder { get; set; }\n\tpublic int TopicCount { get; set; }\n\tpublic int PostCount { get; set; }\n\tpublic DateTime LastPostTime { get; set; }\n\tpublic string LastPostName { get; set; }\n\tpublic string UrlName { get; set; }\n\tpublic string ForumAdapterName { get; set; }\n\tpublic bool IsQAForum { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ForumPermissionContainer.cs",
    "content": "namespace PopForums.Models;\n\npublic class ForumPermissionContainer\n{\n\tpublic int ForumID { get; set; }\n\tpublic List<string> AllRoles { get; set; }\n\tpublic List<string> PostRoles { get; set; }\n\tpublic List<string> ViewRoles { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ForumPermissionContext.cs",
    "content": "namespace PopForums.Models;\n\npublic class ForumPermissionContext\n{\n\tpublic bool UserCanView { get; set; }\n\tpublic bool UserCanPost { get; set; }\n\tpublic bool UserCanModerate { get; set; }\n\tpublic string DenialReason { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ForumState.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class ForumState\n{\n\tpublic int? ForumID { get; set; }\n\tpublic int PageSize { get; set; }\n\tpublic int PageIndex { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ForumTopicContainer.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class ForumTopicContainer : PagedTopicContainer\n{\n\tpublic Forum Forum { get; set; }\n\tpublic ForumPermissionContext PermissionContext { get; set; }\n\tpublic ForumState ForumState { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/IPHistoryEvent.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class IPHistoryEvent\n{\n\tpublic DateTime EventTime { get; set; }\n\tpublic string Type { get; set; }\n\tpublic string Description { get; set; }\n\tpublic int? UserID { get; set; }\n\tpublic string Name { get; set; }\n\tpublic object ID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/IStreamResponse.cs",
    "content": "namespace PopForums.Models;\n\n/// <summary>\n/// This interface is used to pass a Stream to send to the client. It implements IDisposable to allow for cleanup\n/// of the Stream and any other unmanaged resources (like the database connections, for example). Because using\n/// statements would fall out of scope when a controller action returns, the Stream would be disposed before it\n/// could be used. This interface allows for the Stream to be used and then disposed of when the client is done by\n/// registering it with the HttpResponse RegisterForDispose method.\n/// </summary>\npublic interface IStreamResponse : IDisposable\n{\n\tStream Stream { get; }\n}"
  },
  {
    "path": "src/PopForums/Models/Ignore.cs",
    "content": "namespace PopForums.Models;\n\npublic class Ignore\n{\n\tpublic int UserID { get; set; }\n\tpublic int IgnoreUserID { get; set; }\n}\n\npublic class IgnoreWithName : Ignore\n{\n\tpublic string Name { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ModerationLogEntry.cs",
    "content": "namespace PopForums.Models;\n\npublic class ModerationLogEntry\n{\n\tpublic int ModerationID { get; set; }\n\tpublic DateTime TimeStamp { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string UserName { get; set; }\n\tpublic ModerationType ModerationType { get; set; }\n\tpublic string ModerationTypeString => ModerationType.ToString();\n\tpublic int? ForumID { get; set; }\n\tpublic int TopicID { get; set; }\n\tpublic int? PostID { get; set; }\n\tpublic string Comment { get; set; }\n\tpublic string OldText { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ModerationType.cs",
    "content": "namespace PopForums.Models;\n\npublic enum ModerationType\n{\n\tNotSet = 0,\n\tPostEdit = 1,\n\tPostDelete = 2,\n\tPostDeletePermanently = 3,\n\tTopicEdit = 4,\n\tTopicDelete = 5,\n\tTopicDeletePermanently = 6,\n\tTopicClose = 7,\n\tTopicOpen = 8,\n\tTopicPin = 9,\n\tTopicUnpin = 10,\n\tTopicMoved = 11,\n\tTopicUndelete = 12,\n\tPostUndelete = 13,\n\tTopicRenamed = 14,\n\tTopicCloseAuto = 15\n}"
  },
  {
    "path": "src/PopForums/Models/ModifyForumRolesContainer.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class ModifyForumRolesContainer\n{\n\tpublic int ForumID { get; set; }\n\tpublic ModifyForumRolesType ModifyType { get; set; }\n\tpublic string Role { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ModifyForumRolesType.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic enum ModifyForumRolesType\n{\n\tAddView,\n\tRemoveView,\n\tAddPost,\n\tRemovePost,\n\tRemoveAllView,\n\tRemoveAllPost\n}"
  },
  {
    "path": "src/PopForums/Models/NewPost.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class NewPost\n{\n\tpublic string Title { get; set; }\n\tpublic string FullText { get; set; }\n\tpublic bool IncludeSignature { get; set; }\n\t/// <summary>\n\t/// The ForumID or TopicID the post will be associated with.\n\t/// </summary>\n\tpublic int ItemID { get; set; }\n\tpublic bool CloseOnReply { get; set; }\n\tpublic bool IsPlainText { get; set; }\n\tpublic bool IsImageEnabled { get; set; }\n\tpublic int ParentPostID { get; set; }\n\tpublic string[] PostImageIDs { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PagedListOfT.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PagedList<T> : PagerContext where T : class\n{\n\tpublic IEnumerable<T> List { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PagedTopicContainer.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PagedTopicContainer\n{\n\tpublic PagedTopicContainer()\n\t{\n\t\tReadStatusLookup = new Dictionary<int, ReadStatus>();\n\t}\n\n\tpublic List<Topic> Topics { get; set; }\n\tpublic PagerContext PagerContext { get; set; }\n\tpublic Dictionary<int, ReadStatus> ReadStatusLookup { get; private set; }\n\tpublic Dictionary<int, string> ForumTitles { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PagerContext.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PagerContext\n{\n\tpublic int PageCount { get; set; }\n\tpublic int PageIndex { get; set; }\n\tpublic int PageSize { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PasswordResetContainer.cs",
    "content": "namespace PopForums.Models;\n\npublic class PasswordResetContainer\n{\n\tpublic bool IsValidUser { get; set; }\n\tpublic string Password { get; set; }\n\tpublic string PasswordRetype { get; set; }\n\tpublic string Result { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PermanentRoles.cs",
    "content": "namespace PopForums.Models;\n\npublic static class PermanentRoles\n{\n\tpublic const string Admin = \"Admin\";\n\tpublic const string Moderator = \"Moderator\";\n}"
  },
  {
    "path": "src/PopForums/Models/Post.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class Post\n{\n\tpublic int PostID { get; set; }\n\tpublic int TopicID { get; set; }\n\tpublic int ParentPostID { get; set; }\n\tpublic string IP { get; set; }\n\tpublic bool IsFirstInTopic { get; set; }\n\tpublic bool ShowSig { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string Name { get; set; }\n\tpublic string Title { get; set; }\n\tpublic string FullText { get; set; }\n\tpublic DateTime PostTime { get; set; }\n\tpublic bool IsEdited { get; set; }\n\tpublic string LastEditName { get; set; }\n\tpublic DateTime? LastEditTime { get; set; }\n\tpublic bool IsDeleted { get; set; }\n\tpublic int Votes { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PostEdit.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PostEdit\n{\n\tpublic PostEdit() {}\n\n\tpublic PostEdit(Post post)\n\t{\n\t\tTitle = post.Title;\n\t\tFullText = post.FullText;\n\t\tShowSig = post.ShowSig;\n\t}\n\n\tpublic string Title { get; set; }\n\tpublic string FullText { get; set; }\n\tpublic bool ShowSig { get; set; }\n\tpublic string Comment { get; set; }\n\tpublic bool IsPlainText { get; set; }\n\tpublic bool IsFirstInTopic { get; set; }\n\tpublic string[] PostImageIDs { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PostImage.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PostImage\n{\n\tpublic string ID { get; init; }\n\tpublic DateTime TimeStamp { get; init; }\n\tpublic string TenantID { get; init; }\n\tpublic string ContentType { get; init; }\n\tpublic byte[] ImageData { get; init; }\n\n}"
  },
  {
    "path": "src/PopForums/Models/PostImagePersistPayload.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PostImagePersistPayload\n{\n\tpublic string Url { get; set; }\n\tpublic string ID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PostItemContainer.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PostItemContainer\n{\n\tpublic Post Post { get; set; }\n\tpublic List<int> VotedPostIDs { get; set; }\n\tpublic Dictionary <int, string> Signatures { get; set; }\n\tpublic Dictionary<int, int> Avatars { get; set; }\n\tpublic User User { get; set; }\n\tpublic Profile Profile { get; set; }\n\tpublic Topic Topic { get; set; }\n\tpublic List<int> IgnoreUserIDs { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PostWithChildren.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PostWithChildren\n{\n\tpublic Post Post { get; set; }\n\tpublic List<Post> Children { get; set; } \n\tpublic DateTime? LastReadTime { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PrivateMessage.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PrivateMessage\n{\n\tpublic int PMID { get; set; }\n\tpublic DateTime LastPostTime { get; set; }\n\tpublic JsonElement Users { get; set; }\n\tpublic DateTime LastViewDate { get; set; }\n\n\tpublic static string GetUserNames(PrivateMessage pm, int excludeUserID)\n\t{\n\t\tvar users = pm.Users.Deserialize<UserNamePair[]>(new JsonSerializerOptions{PropertyNamingPolicy = JsonNamingPolicy.CamelCase});\n\t\tif (users == null)\n\t\t\treturn string.Empty;\n\t\tvar names = new List<string>();\n\t\tforeach (var item in users)\n\t\t{\n\t\t\tif (item.UserID != excludeUserID)\n\t\t\t\tnames.Add(item.Name as string);\n\t\t}\n\t\tvar result = string.Join(\", \", names);\n\t\treturn result;\n\t}\n\t\n\tprivate class UserNamePair\n\t{\n\t\tpublic int UserID { get; set; }\n\t\tpublic string Name { get; set; }\n\t}\n}"
  },
  {
    "path": "src/PopForums/Models/PrivateMessageBoxType.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic enum PrivateMessageBoxType\n{\n\tInbox = 1,\n\tArchive = 2\n}"
  },
  {
    "path": "src/PopForums/Models/PrivateMessagePost.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PrivateMessagePost\n{\n\tpublic int PMPostID { get; set; }\n\tpublic int PMID { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string Name { get; set; }\n\tpublic DateTime PostTime { get; set; }\n\tpublic string FullText { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PrivateMessageState.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PrivateMessageState\n{\n\tpublic int PmID { get; set; }\n\tpublic JsonElement Users { get; set; }\n\tpublic ClientPrivateMessagePost[] Messages { get; set; }\n\tpublic int? NewestPostID { get; set; }\n\tpublic bool IsUserNotFound { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PrivateMessageUser.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PrivateMessageUser\n{\n\tpublic int PMID { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic DateTime LastViewDate { get; set; }\n\tpublic bool IsArchived { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/PrivateMessageView.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class PrivateMessageView\n{\n\tpublic PrivateMessage PrivateMessage { get; set; }\n\tpublic PrivateMessageState State { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/Profile.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class Profile\n{\n\tpublic int UserID { get; set; }\n\tpublic bool IsSubscribed { get; set; }\n\tpublic string Signature { get; set; }\n\tpublic bool ShowDetails { get; set; }\n\tpublic string Location { get; set; }\n\tpublic bool IsPlainText { get; set; }\n\tpublic DateTime? Dob { get; set; }\n\tpublic string Web { get; set; }\n\tpublic string Facebook { get; set; }\n\tpublic string Instagram { get; set; }\n\tpublic bool IsTos { get; set; }\n\tpublic int? AvatarID { get; set; }\n\tpublic int? ImageID { get; set; }\n\tpublic bool HideVanity { get; set; }\n\tpublic int? LastPostID { get; set; }\n\tpublic int Points { get; set; }\n\tpublic bool IsAutoFollowOnReply { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/QAPostItemContainer.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class QAPostItemContainer : PostItemContainer\n{\n\tpublic PostWithChildren PostWithChildren { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/QueuedEmailMessage.cs",
    "content": "namespace PopForums.Models;\n\npublic class QueuedEmailMessage : EmailMessage\n{\n\tpublic int MessageID { get; set; }\n\tpublic DateTime QueueTime { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/ReadStatus.cs",
    "content": "namespace PopForums.Models;\n\n[Flags]\npublic enum ReadStatus\n{\n\tNoNewPosts = 1,\n\tNewPosts = 2,\n\tClosed = 4,\n\tOpen = 8,\n\tPinned = 16,\n\tNotPinned = 32\n}"
  },
  {
    "path": "src/PopForums/Models/ResponseOfT.cs",
    "content": "﻿namespace PopForums.Models;\n\n/// <summary>\n/// A generic container for wrapping the response of external calls for consumption by internal service.\n/// </summary>\n/// <typeparam name=\"T\"></typeparam>\npublic class Response<T> where T : class\n{\n\t/// <summary>\n\t/// Creates a generic Response with IsValid set to true and no debug information or exception.\n\t/// </summary>\n\t/// <param name=\"data\">The strongly typed result to be consumed by the caller.</param>\n\tpublic Response(T data)\n\t{\n\t\tData = data;\n\t\tIsValid = true;\n\t}\n\n\t/// <summary>\n\t/// Creates a generic response with all fields set.\n\t/// </summary>\n\t/// <param name=\"data\"></param>\n\t/// <param name=\"isValid\">Default is false.</param>\n\t/// <param name=\"exception\">Default is null.</param>\n\t/// <param name=\"debugInfo\">Default is null.</param>\n\tpublic Response(T data, bool isValid = false, Exception exception = null, string debugInfo = null)\n\t{\n\t\tData = data;\n\t\tIsValid = isValid;\n\t\tException = exception;\n\t\tDebugInfo = debugInfo;\n\t}\n\t\t\n\tpublic T Data { get; }\n\tpublic bool IsValid { get; }\n\tpublic Exception Exception { get; }\n\tpublic string DebugInfo { get; }\n}"
  },
  {
    "path": "src/PopForums/Models/SearchIndexPayload.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class SearchIndexPayload\n{\n\tpublic int TopicID { get; set; }\n\tpublic string TenantID { get; set; }\n\tpublic bool IsForRemoval { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/SearchType.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic enum SearchType\n{\n\tRank,\n\tDate,\n\tTitle,\n\tName,\n\tReplies\n}"
  },
  {
    "path": "src/PopForums/Models/SearchWord.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class SearchWord\n{\n\tpublic string Word { get; set; }\n\tpublic int TopicID { get; set; }\n\tpublic int Rank { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/SecurityLogEntry.cs",
    "content": "namespace PopForums.Models;\n\npublic class SecurityLogEntry\n{\n\tpublic int SecurityLogID { get; set; }\n\tpublic SecurityLogType SecurityLogType { get; set; }\n\tpublic string SecurityLogTypeString => SecurityLogType.ToString();\n\tpublic int? UserID { get; set; }\n\tpublic int? TargetUserID { get; set; }\n\tpublic string IP { get; set; }\n\tpublic string Message { get; set; }\n\tpublic DateTime ActivityDate { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/SecurityLogType.cs",
    "content": "namespace PopForums.Models;\n\npublic enum SecurityLogType\n{\n\tUndefined = 0,\n\tLogin = 1,\n\tLogout = 2,\n\tPasswordChange = 3,\n\tEmailChange = 4,\n\tFailedLogin = 5,\n\tUserCreated = 6,\n\tUserDeleted = 7,\n\tRoleCreated = 8,\n\tRoleDeleted = 9,\n\tUserAddedToRole = 10,\n\tUserRemovedFromRole = 11,\n\tUserSessionStart = 12,\n\tUserSessionEnd = 13,\n\tNameChange = 14,\n\tIsApproved = 15,\n\tIsNotApproved = 16,\n\tExternalAssociationSet = 17,\n\tExternalAssociationRemoved = 18,\n\tExternalAssociationCheckSuccessful = 19,\n\tExternalAssociationCheckFailed = 20,\n\tReCaptchaFailed = 21,\n\tExternalLoginChallengeFailed = 22\n}"
  },
  {
    "path": "src/PopForums/Models/ServiceHeartbeat.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class ServiceHeartbeat\n{\n\tpublic string ServiceName { get; set; }\n\tpublic string MachineName { get; set; }\n\tpublic DateTime LastRun { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/SetupVariables.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class SetupVariables\n{\n\tpublic string Name { get; set; }\n\tpublic string Email { get; set; }\n\tpublic string Password { get; set; }\n\tpublic string ForumTitle { get; set; }\n\tpublic string SmtpServer { get; set; }\n\tpublic int SmtpPort { get; set; }\n\tpublic string MailerAddress { get; set; }\n\tpublic bool UseEsmtp { get; set; }\n\tpublic string SmtpUser { get; set; }\n\tpublic string SmtpPassword { get; set; }\n\tpublic bool UseSslSmtp { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/SignupData.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class SignupData\n{\n\tpublic string Name { get; set; }\n\tpublic string Email { get; set; }\n\tpublic string Password { get; set; }\n\tpublic string PasswordRetype { get; set; }\n\tpublic bool IsSubscribed { get; set; }\n\tpublic bool IsCoppa { get; set; }\n\tpublic bool IsTos { get; set; }\n\tpublic string Token { get; set; }\n\tpublic bool IsAutoFollowOnReply { get; set; }\n\n\tpublic static string GetCoppaDate()\n\t{\n\t\treturn DateTime.Now.AddYears(-13).ToString(\"D\");\n\t}\n}"
  },
  {
    "path": "src/PopForums/Models/SingleString.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class SingleString\n{\n\tpublic string String { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/SubscribeNotificationPayload.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class SubscribeNotificationPayload\n{\n\tpublic int TopicID { get; set; }\n\tpublic string TopicTitle { get; set; }\n\tpublic int PostingUserID { get; set; }\n\tpublic string PostingUserName { get; set; }\n\tpublic string TenantID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/TimeFormats.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class TimeFormats\n{\n\tpublic string TodayTime { get; set; }\n\tpublic string YesterdayTime { get; set; }\n\tpublic string MinutesAgo { get; set; }\n\tpublic string OneMinuteAgo { get; set; }\n\tpublic string LessThanMinute { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/Topic.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class Topic\n{\n\tpublic int TopicID { get; set; }\n\tpublic int ForumID { get; set; }\n\tpublic string Title { get; set; }\n\tpublic int ReplyCount { get; set; }\n\tpublic int ViewCount { get; set; }\n\tpublic int StartedByUserID { get; set; }\n\tpublic string StartedByName { get; set; }\n\tpublic int LastPostUserID { get; set; }\n\tpublic string LastPostName { get; set; }\n\tpublic DateTime LastPostTime { get; set; }\n\tpublic bool IsClosed { get; set; }\n\tpublic bool IsPinned { get; set; }\n\tpublic bool IsDeleted { get; set; }\n\tpublic string UrlName { get; set; }\n\tpublic int? AnswerPostID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/TopicContainer.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class TopicContainer\n{\n\tpublic Forum Forum { get; set; }\n\tpublic Topic Topic { get; set; }\n\tpublic List<Post> Posts { get; set; }\n\tpublic PagerContext PagerContext { get; set; }\n\tpublic ForumPermissionContext PermissionContext { get; set; }\n\tpublic Dictionary<int, string> Signatures { get; set; }\n\tpublic Dictionary<int, int> Avatars { get; set; }\n\tpublic List<int> VotedPostIDs { get; set; }\n\tpublic DateTime? LastReadTime { get; set; }\n\tpublic TopicState TopicState { get; set; }\n\tpublic List<int> IgnoreUserIDs { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/TopicContainerForQA.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class TopicContainerForQA : TopicContainer\n{\n\tpublic PostWithChildren QuestionPostWithComments { get; set; }\n\tpublic List<PostWithChildren> AnswersWithComments { get; set; } \n}"
  },
  {
    "path": "src/PopForums/Models/TopicState.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class TopicState\n{\n\tpublic int TopicID { get; set; }\n\tpublic bool IsImageEnabled { get; set; }\n\tpublic bool IsSubscribed { get; set; }\n\tpublic bool IsFavorite { get; set; }\n\tpublic int? PageIndex { get; set; }\n\tpublic int? PageCount { get; set; }\n\tpublic int LastVisiblePostID { get; set; }\n\tpublic int? AnswerPostID { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/TopicUnsubscribeContainer.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class TopicUnsubscribeContainer\n{\n\tpublic User User { get; set; }\n\tpublic Topic Topic { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/User.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class User\n{\n\tpublic int UserID { get; set; }\n\tpublic DateTime CreationDate { get; set; }\n\tpublic string Name { get; set; }\n\tpublic string Email { get; set; }\n\tpublic Guid AuthorizationKey { get; set; }\n\tpublic bool IsApproved { get; set; }\n\tpublic DateTime? TokenExpiration { get; set; }\n\tpublic List<string> Roles { get; set; }\n\n\tpublic bool IsInRole(string role)\n\t{\n\t\tif (Roles == null)\n\t\t\tthrow new Exception(\"Roles not set for user.\");\n\t\treturn Roles.Contains(role);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Models/UserEdit.cs",
    "content": "namespace PopForums.Models;\n\npublic class UserEdit\n{\n\tpublic UserEdit() {}\n\n\tpublic UserEdit(User user, Profile profile)\n\t{\n\t\tUserID = user.UserID;\n\t\tName = user.Name;\n\t\tEmail = user.Email;\n\t\tIsApproved = user.IsApproved;\n\t\tIsSubscribed = profile.IsSubscribed;\n\t\tSignature = profile.Signature;\n\t\tShowDetails = profile.ShowDetails;\n\t\tLocation = profile.Location;\n\t\tIsPlainText = profile.IsPlainText;\n\t\tDob = profile.Dob;\n\t\tWeb = profile.Web;\n\t\tInstagram = profile.Instagram;\n\t\tFacebook = profile.Facebook;\n\t\tHideVanity = profile.HideVanity;\n\t\tRoles = user.Roles.ToArray();\n\t\tAvatarID = profile.AvatarID;\n\t\tImageID = profile.ImageID;\n\t\tIsAutoFollowOnReply = profile.IsAutoFollowOnReply;\n\t}\n\n\tpublic int UserID { get; set; }\n\tpublic string Name { get; set; }\n\tpublic string Email { get; set; }\n\tpublic string NewEmail { get; set; }\n\tpublic string NewPassword { get; set; }\n\tpublic bool IsApproved { get; set; }\n\tpublic bool IsSubscribed { get; set; }\n\tpublic string Signature { get; set; }\n\tpublic bool ShowDetails { get; set; }\n\tpublic string Location { get; set; }\n\tpublic bool IsPlainText { get; set; }\n\tpublic DateTime? Dob { get; set; }\n\tpublic string Web { get; set; }\n\tpublic string Instagram { get; set; }\n\tpublic string Facebook { get; set; }\n\tpublic bool HideVanity { get; set; }\n\tpublic string[] Roles { get; set; }\n\tpublic int? AvatarID { get; set; }\n\tpublic int? ImageID { get; set; }\n\tpublic bool DeleteAvatar { get; set; }\n\tpublic bool DeleteImage { get; set; }\n\tpublic bool IsAutoFollowOnReply { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/UserEditProfile.cs",
    "content": "namespace PopForums.Models;\n\npublic class UserEditProfile\n{\n\tpublic UserEditProfile() {}\n\n\tpublic UserEditProfile(Profile profile)\n\t{\n\t\tIsSubscribed = profile.IsSubscribed;\n\t\tSignature = profile.Signature;\n\t\tShowDetails = profile.ShowDetails;\n\t\tLocation = profile.Location;\n\t\tIsPlainText = profile.IsPlainText;\n\t\tDob = profile.Dob;\n\t\tWeb = profile.Web;\n\t\tInstagram = profile.Instagram;\n\t\tFacebook = profile.Facebook;\n\t\tHideVanity = profile.HideVanity;\n\t\tIsAutoFollowOnReply = profile.IsAutoFollowOnReply;\n\t}\n\n\tpublic bool IsSubscribed { get; set; }\n\tpublic string Signature { get; set; }\n\tpublic bool ShowDetails { get; set; }\n\tpublic string Location { get; set; }\n\tpublic bool IsPlainText { get; set; }\n\tpublic DateTime? Dob { get; set; }\n\tpublic string Web { get; set; }\n\tpublic string Instagram { get; set; }\n\tpublic string Facebook { get; set; }\n\tpublic bool HideVanity { get; set; }\n\tpublic bool IsAutoFollowOnReply { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/UserEditSecurity.cs",
    "content": "namespace PopForums.Models;\n\npublic class UserEditSecurity\n{\n\tpublic UserEditSecurity() {}\n\n\tpublic UserEditSecurity(User user, bool isNewUserApproved)\n\t{\n\t\tOldEmail = user.Email;\n\t\tIsNewUserApproved = isNewUserApproved;\n\t}\n\n\tpublic string OldPassword { get; set; }\n\tpublic string NewPassword { get; set; }\n\tpublic string NewPasswordRetype { get; set; }\n\tpublic string OldEmail { get; private set; }\n\tpublic string NewEmail { get; set; }\n\tpublic string NewEmailRetype { get; set; }\n\tpublic bool IsNewUserApproved { get; set; }\n\n\tpublic bool NewPasswordsMatch()\n\t{\n\t\tif (String.IsNullOrWhiteSpace(NewPassword) || String.IsNullOrWhiteSpace(NewPasswordRetype))\n\t\t\treturn false;\n\t\treturn NewPassword == NewPasswordRetype;\n\t}\n\n\tpublic bool NewEmailsMatch()\n\t{\n\t\tif (String.IsNullOrWhiteSpace(NewEmail) || String.IsNullOrWhiteSpace(NewEmailRetype))\n\t\t\treturn false;\n\t\treturn NewEmail == NewEmailRetype;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Models/UserImage.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class UserImage\n{\n\tpublic int UserImageID { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic int SortOrder { get; set; }\n\tpublic bool IsApproved { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/UserImageApprovalContainer.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class UserImageApprovalContainer\n{\n\tpublic bool IsNewUserImageApproved { get; set; }\n\tpublic List<UserImagePair> Unapproved { get; set; }\n}\n\npublic class UserImagePair\n{\n\tpublic User User { get; set; }\n\tpublic UserImage UserImage { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/UserResult.cs",
    "content": "﻿namespace PopForums.Models;\n\npublic class UserResult\n{\n\tpublic int UserID { get; set; }\n\tpublic string Name { get; set; }\n\tpublic string Email { get; set; }\n\tpublic DateTime CreationDate { get; set; }\n\tpublic string IP { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Models/UserSearch.cs",
    "content": "namespace PopForums.Models;\n\npublic class UserSearch\n{\n\tpublic string SearchText { get; set; }\n\tpublic UserSearchType SearchType { get; set; }\n\n\tpublic enum UserSearchType\n\t{\n\t\tName, Email, Role\n\t}\n}"
  },
  {
    "path": "src/PopForums/Models/VotePostContainer.cs",
    "content": "namespace PopForums.Models;\n\npublic class VotePostContainer\n{\n\tpublic int PostID { get; set; }\n\tpublic int Votes { get; set; }\n\tpublic Dictionary<int, string> Voters { get; set; }\n}"
  },
  {
    "path": "src/PopForums/PopForums.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<Description>PopForums Class Library</Description>\n\t\t<VersionPrefix>22.0.0</VersionPrefix>\n\t\t<Authors>Jeff Putz</Authors>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<AssemblyName>PopForums</AssemblyName>\n\t\t<PackageId>PopForums</PackageId>\n\t\t<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>\n\t\t<PackageProjectUrl>https://github.com/POPWorldMedia/POPForums</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/POPWorldMedia/POPForums</RepositoryUrl>\n\t\t<Copyright>2025, POP World Media, LLC</Copyright>\n\t\t<PackageLicenseExpression>MIT</PackageLicenseExpression>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<EmbeddedResource Include=\"Resources\\*.resx\" Exclude=\"bin\\**;obj\\**;**\\*.xproj;packages\\**;@(EmbeddedResource)\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"MailKit\" Version=\"4.16.0\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.Configuration\" Version=\"10.0.7\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.Configuration.Json\" Version=\"10.0.7\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"10.0.7\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" Version=\"10.0.7\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.Logging\" Version=\"10.0.7\" />\n\t\t<PackageReference Include=\"SixLabors.ImageSharp\" Version=\"3.1.12\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Compile Update=\"Resources\\Resources.Designer.cs\">\n\t\t\t<DesignTime>True</DesignTime>\n\t\t\t<AutoGen>True</AutoGen>\n\t\t\t<DependentUpon>Resources.resx</DependentUpon>\n\t\t</Compile>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<EmbeddedResource Update=\"Resources\\Resources.resx\">\n\t\t\t<Generator>PublicResXFileCodeGenerator</Generator>\n\t\t\t<LastGenOutput>Resources.Designer.cs</LastGenOutput>\n\t\t\t<CustomToolNamespace>PopForums</CustomToolNamespace>\n\t\t</EmbeddedResource>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Update=\"Microsoft.NETCore.App\" />\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/PopForums/Repositories/IAwardCalculationQueueRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IAwardCalculationQueueRepository\n{\n\tTask Enqueue(AwardCalculationPayload payload);\n\tTask<KeyValuePair<string, int>> Dequeue();\n}"
  },
  {
    "path": "src/PopForums/Repositories/IAwardConditionRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IAwardConditionRepository\n{\n\tTask<List<AwardCondition>> GetConditions(string awardDefinitionID);\n\tTask DeleteConditions(string awardDefinitionID);\n\tTask SaveConditions(List<AwardCondition> conditions);\n\tTask DeleteConditionsByEventDefinitionID(string eventDefinitionID);\n\tTask DeleteCondition(string awardDefinitionID, string eventDefinitionID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IAwardDefinitionRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IAwardDefinitionRepository\n{\n\tTask<AwardDefinition> Get(string awardDefinitionID);\n\tTask<List<AwardDefinition>> GetAll();\n\tTask<List<AwardDefinition>> GetByEventDefinitionID(string eventDefinitionID);\n\tTask Create(string awardDefinitionID, string title, string description, bool isSingleTimeAward);\n\tTask Delete(string awardDefinitionID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IBanRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface IBanRepository\n{\n\tTask BanIP(string ip);\n\tTask RemoveIPBan(string ip);\n\tTask<List<string>> GetIPBans();\n\tTask<bool> IPIsBanned(string ip);\n\tTask BanEmail(string email);\n\tTask RemoveEmailBan(string email);\n\tTask<List<string>> GetEmailBans();\n\tTask<bool> EmailIsBanned(string email);\n}"
  },
  {
    "path": "src/PopForums/Repositories/ICategoryRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface ICategoryRepository\n{\n\tTask<Category> Get(int categoryID);\n\tTask<List<Category>> GetAll();\n\tTask<Category> Create(string newTitle, int sortOrder);\n\tTask Delete(int categoryID);\n\tTask Update(Category category);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IEmailQueueRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IEmailQueueRepository\n{\n\tTask Enqueue(EmailQueuePayload payload);\n\tTask<EmailQueuePayload> Dequeue();\n}"
  },
  {
    "path": "src/PopForums/Repositories/IErrorLogRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface IErrorLogRepository\n{\n\tTask<ErrorLogEntry> Create(DateTime timeStamp, string message, string stackTrace, string data, ErrorSeverity severity);\n\tTask<int> GetErrorCount();\n\tTask<List<ErrorLogEntry>> GetErrors(int startRow, int pageSize);\n\tTask DeleteError(int errorID);\n\tTask DeleteAllErrors();\n}"
  },
  {
    "path": "src/PopForums/Repositories/IEventDefinitionRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IEventDefinitionRepository\n{\n\tTask<EventDefinition> Get(string eventDefinitionID);\n\tTask<List<EventDefinition>> GetAll();\n\tTask Create(EventDefinition eventDefinition);\n\tvoid Delete(string eventDefinitionID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IExternalUserAssociationRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IExternalUserAssociationRepository\n{\n\tTask<ExternalUserAssociation> Get(string issuer, string providerKey);\n\tTask<ExternalUserAssociation> Get(int externalUserAssociationID);\n\tTask<List<ExternalUserAssociation>> GetByUser(int userID);\n\tTask Save(int userID, string issuer, string providerKey, string name);\n\tTask Delete(int externalUserAssociationID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IFavoriteTopicsRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IFavoriteTopicsRepository\n{\n\tTask<List<Topic>> GetFavoriteTopics(int userID, int startRow, int pageSize);\n\tTask<int> GetFavoriteTopicCount(int userID);\n\tTask<bool> IsTopicFavorite(int userID, int topicID);\n\tTask AddFavoriteTopic(int userID, int topicID);\n\tTask RemoveFavoriteTopic(int userID, int topicID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IFeedRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface IFeedRepository\n{\n\tTask<List<FeedEvent>> GetFeed(int userID, int itemCount);\n\tTask PublishEvent(int userID, string message, int points, DateTime timeStamp);\n\tTask<DateTime> GetOldestTime(int userID, int takeCount);\n\tTask DeleteOlderThan(int userID, DateTime timeCutOff);\n\tTask<List<FeedEvent>> GetFeed(int itemCount);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IForumRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IForumRepository\n{\n\tTask<Forum> Get(int forumID);\n\tTask<Forum> Get(string urlName);\n\tTask<Forum> Create(int? categoryID, string title, string description, bool isVisible, bool isArchived, int sortOrder, string urlName, string forumAdapterName, bool isQAForum);\n\tTask<List<Forum>> GetForumsInCategory(int? categoryID);\n\tTask<List<string>> GetUrlNamesThatStartWith(string urlName);\n\tTask Update(int forumID, int? categoryID, string title, string description, bool isVisible, bool isArchived, string urlName, string forumAdapterName, bool isQAForum);\n\tTask UpdateSortOrder(int forumID, int newSortOrder);\n\tTask UpdateCategoryAssociation(int forumID, int? categoryID);\n\tTask UpdateLastTimeAndUser(int forumID, DateTime lastTime, string lastName);\n\tTask UpdateTopicAndPostCounts(int forumID, int topicCount, int postCount);\n\tTask IncrementPostCount(int forumID);\n\tTask IncrementPostAndTopicCount(int forumID);\n\tTask<IEnumerable<Forum>> GetAll();\n\tTask<IEnumerable<Forum>> GetAllVisible();\n\n\t/// <summary>\n\t/// Gets role requirements for the ability to post in this forum.\n\t/// </summary>\n\t/// <remarks>This should generally be cached, because the calling code runs this test on every forum when displayed.</remarks>\n\t/// <param name=\"forumID\">ID of forum to fetch posting roles for.</param>\n\t/// <returns>A List of strings for roles required for posting.</returns>\n\tTask<List<string>> GetForumPostRoles(int forumID);\n\n\t/// <summary>\n\t/// Gets role requirements for the ability to view this forum.\n\t/// </summary>\n\t/// <remarks>This should generally be cached, because the calling code runs this test on every forum when displayed.</remarks>\n\t/// <param name=\"forumID\">ID of forum to fetch roles required for viewing.</param>\n\t/// <returns>A List of strings for roles required for viewing.</returns>\n\tTask<List<string>> GetForumViewRoles(int forumID);\n\n\t/// <summary>\n\t/// Gets a graph of forums and associated post restrictions.\n\t/// </summary>\n\t/// <remarks>This should generally be cached, because the calling code runs this test on every forum when displayed.</remarks>\n\t/// <returns>A dictionary of key/value pairs of forums and post role restrictions.</returns>\n\tTask<Dictionary<int, List<string>>> GetForumPostRestrictionRoleGraph();\n\n\t/// <summary>\n\t/// Gets a graph of forums and associated view restrictions.\n\t/// </summary>\n\t/// <remarks>This should generally be cached, because the calling code runs this test on every forum when displayed.</remarks>\n\t/// <returns>A dictionary of key/value pairs of forums and view role restrictions.</returns>\n\tTask<Dictionary<int, List<string>>> GetForumViewRestrictionRoleGraph();\n\n\tTask AddPostRole(int forumID, string role);\n\tTask RemovePostRole(int forumID, string role);\n\tTask AddViewRole(int forumID, string role);\n\tTask RemoveViewRole(int forumID, string role);\n\tTask RemoveAllPostRoles(int forumID);\n\tTask RemoveAllViewRoles(int forumID);\n\n\t/// <summary>\n\t/// Gets all UrlName values for all forums.\n\t/// </summary>\n\t/// <remarks>This should generally be cached, as it's used as a lookup for URL routing on every request.</remarks>\n\t/// <returns>An enumerable object of strings.</returns>\n\tTask<IEnumerable<string>> GetAllForumUrlNames();\n\n\tDictionary<int, string> GetAllForumTitles();\n\tTask<int> GetAggregateTopicCount();\n\tTask<int> GetAggregatePostCount();\n}"
  },
  {
    "path": "src/PopForums/Repositories/IIgnoreRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface IIgnoreRepository\n{\n\tTask AddIgnore(int userID, int ignoreUserID);\n\tTask DeleteIgnore(int userID, int ignoreUserID);\n\tTask<IList<IgnoreWithName>> GetIgnoreList(int userID);\n\tTask<List<int>> GetIgnoredUserIdsInList(int userID, List<int> userIDs);\n}"
  },
  {
    "path": "src/PopForums/Repositories/ILastReadRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface ILastReadRepository\n{\n\tTask SetForumRead(int userID, int forumID, DateTime readTime);\n\tTask DeleteTopicReadsInForum(int userID, int forumID);\n\tTask SetAllForumsRead(int userID, DateTime readTime);\n\tTask DeleteAllTopicReads(int userID);\n\tTask SetTopicRead(int userID, int topicID, DateTime readTime);\n\tTask<Dictionary<int, DateTime>> GetLastReadTimesForForums(int userID);\n\tTask<DateTime?> GetLastReadTimesForForum(int userID, int forumID);\n\tTask<Dictionary<int, DateTime>> GetLastReadTimesForTopics(int userID, IEnumerable<int> topicIDs);\n\tTask<DateTime?> GetLastReadTimeForTopic(int userID, int topicID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IModerationLogRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface IModerationLogRepository\n{\n\tTask Log(DateTime timeStamp, int userID, string userName, int moderationType, int? forumID, int topicID, int? postID, string comment, string oldText);\n\tTask<List<ModerationLogEntry>> GetLog(DateTime start, DateTime end);\n\tTask<List<ModerationLogEntry>> GetLog(int topicID, bool excludePostEntries);\n\tTask<List<ModerationLogEntry>> GetLog(int postID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/INotificationRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface INotificationRepository\n{\n\tTask<int> UpdateNotification(Notification notification);\n\tTask CreateNotification(Notification notification);\n\tTask MarkNotificationRead(int userID, NotificationType notificationType, long contextID);\n\tTask<List<Notification>> GetNotifications(int userID, DateTime afterDateTime, int pageSize);\n\tTask<int> GetPageCount(int userID, int pageSize);\n\tTask<int> GetUnreadNotificationCount(int userID);\n\tTask MarkAllRead(int userID);\n\tTask DeleteOlderThan(int userID, DateTime timeCutOff);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IPointLedgerRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IPointLedgerRepository\n{\n\tTask RecordEntry(PointLedgerEntry entry);\n\tTask<int> GetPointTotal(int userID);\n\tTask<int> GetEntryCount(int userID, string eventDefinitionID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IPostImageRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IPostImageRepository\n{\n\tTask<PostImagePersistPayload> Persist(byte[] bytes, string contentType);\n\tTask<PostImage> GetWithoutData(string id);\n\t[Obsolete(\"Use the combination of GetWithoutData(int) and GetImageStream(int) instead.\")]\n\tTask<PostImage> Get(string id);\n\tTask DeletePostImageData(string id, string tenantID);\n\tTask<IStreamResponse> GetImageStream(string id);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IPostImageTempRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IPostImageTempRepository\n{\n\tTask Save(Guid postImageTempID, DateTime timeStamp, string tenantID);\n\tTask Delete(Guid id);\n\tTask<List<Guid>> GetOld(DateTime olderThan);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IPostRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IPostRepository\n{\n\tTask<int> Create(int topicID, int parentPostID, string IP, bool isFirstInTopic, bool showSig, int userID, string name, string title, string fullText, DateTime postTime, bool isEdited, string lastEditName, DateTime? lastEditTime, bool isDeleted, int votes);\n\tTask<bool> Update(Post post);\n\tTask<List<Post>> Get(int topicID, bool includeDeleted, int startRow, int pageSize);\n\tTask<List<Post>> Get(int topicID, bool includeDeleted);\n\tTask<int> GetReplyCount(int topicID, bool includeDeleted);\n\tTask<Post> Get(int postID);\n\tTask<Dictionary<int, DateTime>> GetPostIDsWithTimes(int topicID, bool includeDeleted);\n\tTask<int> GetPostCount(int userID);\n\tTask<List<IPHistoryEvent>> GetIPHistory(string ip, DateTime start, DateTime end);\n\tTask<int> GetLastPostID(int topicID);\n\tTask<int> GetVoteCount(int postID);\n\tTask<int> CalculateVoteCount(int postID);\n\tTask SetVoteCount(int postID, int votes);\n\tTask VotePost(int postID, int userID);\n\tTask<Dictionary<int, string>> GetVotes(int postID);\n\tTask<List<int>> GetVotedPostIDs(int userID, List<int> postIDs);\n\tTask<Post> GetLastInTopic(int topicID);\n\tTask DeleteVote(int postID, int userID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IPrivateMessageRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IPrivateMessageRepository\n{\n\tTask<PrivateMessage> Get(int pmID, int userID);\n\tTask<List<PrivateMessagePost>> GetPosts(int pmID, DateTime afterDateTime);\n\tTask<List<PrivateMessagePost>> GetPosts(int pmID, DateTime beforeDateTime, int pageSize);\n\tTask<int> CreatePrivateMessage(PrivateMessage pm);\n\tTask AddUsers(int pmID, List<int> userIDs, DateTime viewDate, bool isArchived);\n\tTask<int> AddPost(PrivateMessagePost post);\n\tTask<List<PrivateMessageUser>> GetUsers(int pmID);\n\tTask SetLastViewTime(int pmID, int userID, DateTime viewDate);\n\tTask SetArchive(int pmID, int userID, bool isArchived);\n\tTask<List<PrivateMessage>> GetPrivateMessages(int userID, PrivateMessageBoxType boxType, int startRow, int pageSize);\n\tTask<int> GetUnreadCount(int userID);\n\tTask<int> GetBoxCount(int userID, PrivateMessageBoxType boxType);\n\tTask UpdateLastPostTime(int pmID, DateTime lastPostTime);\n\tTask<int> GetExistingFromIDs(List<int> ids);\n\tTask<int?> GetFirstUnreadPostID(int pmID, DateTime lastReadTime);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IProfileRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IProfileRepository\n{\n\tTask<Profile> GetProfile(int userID);\n\tTask Create(Profile profile);\n\tTask<bool> Update(Profile profile);\n\tTask<int?> GetLastPostID(int userID);\n\tTask<bool> SetLastPostID(int userID, int postID);\n\tTask<Dictionary<int, string>> GetSignatures(List<int> userIDs);\n\tTask<Dictionary<int, int>> GetAvatars(List<int> userIDs);\n\tTask SetCurrentImageIDToNull(int userID);\n\tTask UpdatePoints(int userID, int points);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IQueuedEmailMessageRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IQueuedEmailMessageRepository\n{\n\tTask<int> CreateMessage(QueuedEmailMessage message);\n\tTask DeleteMessage(int messageID);\n\tTask<QueuedEmailMessage> GetMessage(int messageID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IRoleRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface IRoleRepository\n{\n\tTask CreateRole(string role);\n\tTask<bool> DeleteRole(string role);\n\tTask<List<string>> GetAllRoles();\n\tTask<List<string>> GetUserRoles(int userID);\n\tTask ReplaceUserRoles(int userID, string[] roles);\n}"
  },
  {
    "path": "src/PopForums/Repositories/ISearchIndexQueueRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface ISearchIndexQueueRepository\n{\n\tTask Enqueue(SearchIndexPayload payload);\n\tTask<SearchIndexPayload> Dequeue();\n}"
  },
  {
    "path": "src/PopForums/Repositories/ISearchRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface ISearchRepository\n{\n\tTask<List<string>> GetJunkWords();\n\tTask CreateJunkWord(string word);\n\tTask DeleteJunkWord(string word);\n\tTask DeleteAllIndexedWordsForTopic(int topicID);\n\tTask SaveSearchWord(int topicID, string word, int rank);\n\tTask<Tuple<Response<List<Topic>>, int>> SearchTopics(string searchTerm, List<int> hiddenForums, SearchType searchType, int startRow, int pageSize);\n}"
  },
  {
    "path": "src/PopForums/Repositories/ISecurityLogRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface ISecurityLogRepository\n{\n\tTask Create(SecurityLogEntry logEntry);\n\tTask<List<SecurityLogEntry>> GetByUserID(int userID, DateTime startDate, DateTime endDate);\n\tTask<List<IPHistoryEvent>> GetIPHistory(string ip, DateTime start, DateTime end);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IServiceHeartbeatRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IServiceHeartbeatRepository\n{\n\tTask RecordHeartbeat(string serviceName, string machineName, DateTime lastRun);\n\tTask<List<ServiceHeartbeat>> GetAll();\n\tTask ClearAll();\n}"
  },
  {
    "path": "src/PopForums/Repositories/ISettingsRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface ISettingsRepository\n{\n\tDictionary<string, string> Get();\n\tvoid Save(Dictionary<string, object> dictionary);\n\tevent Action OnSettingsInvalidated;\n}"
  },
  {
    "path": "src/PopForums/Repositories/ISetupRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface ISetupRepository\n{\n\tbool IsConnectionPossible();\n\tbool IsDatabaseSetup();\n\tvoid SetupDatabase();\n}"
  },
  {
    "path": "src/PopForums/Repositories/ISubscribeNotificationRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface ISubscribeNotificationRepository\n{\n\tTask Enqueue(SubscribeNotificationPayload payload);\n\tTask<SubscribeNotificationPayload> Dequeue();\n}"
  },
  {
    "path": "src/PopForums/Repositories/ISubscribedTopicsRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface ISubscribedTopicsRepository\n{\n\tTask<List<Topic>> GetSubscribedTopics(int userID, int startRow, int pageSize);\n\tTask<int> GetSubscribedTopicCount(int userID);\n\tTask<bool> IsTopicSubscribed(int userID, int topicID);\n\tTask AddSubscribedTopic(int userID, int topicID);\n\tTask RemoveSubscribedTopic(int userID, int topicID);\n\tTask<List<int>> GetSubscribedUserIDs(int topicID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/ITopicRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface ITopicRepository\n{\n\tTask<Topic> GetLastUpdatedTopic(int forumID);\n\tTask<int> GetTopicCount(int forumID, bool includeDeleted);\n\tTask<int> GetTopicCountByUser(int userID, bool includeDeleted, List<int> excludedForums);\n\tTask<int> GetTopicCount(bool includeDeleted, List<int> excludedForums);\n\tTask<int> GetPostCount(int forumID, bool includeDelete);\n\tTask<Topic> Get(int topicID);\n\tTask<Topic> Get(string urlName);\n\tTask<List<Topic>> Get(int forumID, bool includeDeleted, int startRow, int pageSize);\n\tTask<List<Topic>> GetTopicsByUser(int userID, bool includeDeleted, List<int> excludedForums, int startRow, int pageSize);\n\tTask<List<Topic>> Get(bool includeDeleted, List<int> excludedForums, int startRow, int pageSize);\n\tTask<List<Topic>> Get(int forumID, bool includeDeleted, List<int> excludedForums);\n\tTask<List<string>> GetUrlNamesThatStartWith(string urlName);\n\tTask<int> Create(int forumID, string title, int replyCount, int viewCount, int startedByUserID, string startedByName, int lastPostUserID, string lastPostName, DateTime lastPostTime, bool isClosed, bool isPinned, bool isDeleted, string urlName);\n\tTask IncrementReplyCount(int topicID);\n\tTask IncrementViewCount(int topicID);\n\tTask UpdateLastTimeAndUser(int topicID, int userID, string name, DateTime postTime);\n\tTask CloseTopic(int topicID);\n\tTask OpenTopic(int topicID);\n\tTask PinTopic(int topicID);\n\tTask UnpinTopic(int topicID);\n\tTask DeleteTopic(int topicID);\n\tTask UndeleteTopic(int topicID);\n\tTask UpdateTitleAndForum(int topicID, int forumID, string newTitle, string newUrlName);\n\tTask UpdateReplyCount(int topicID, int replyCount);\n\tTask<DateTime?> GetLastPostTime(int topicID);\n\tTask HardDeleteTopic(int topicID);\n\tTask UpdateAnswerPostID(int topicID, int? postID);\n\tTask<List<Topic>> Get(IEnumerable<int> topicIDs);\n\tTask<IEnumerable<int>> CloseTopicsOlderThan(DateTime cutoffDate);\n\tTask<List<Tuple<string, DateTime>>> GetUrlNames(bool includeDeleted, List<int> excludedForums, int startRow, int pageSize);\n}"
  },
  {
    "path": "src/PopForums/Repositories/ITopicViewLogRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface ITopicViewLogRepository\n{\n\tTask Log(int? userID, int topicID, DateTime timeStamp);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IUserAvatarRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IUserAvatarRepository\n{\n\t[Obsolete(\"Use GetImageData(int) instead.\")]\n\tTask<byte[]> GetImageData(int userAvatarID);\n\tTask<IStreamResponse> GetImageStream(int userAvatarID);\n\tTask<List<int>> GetUserAvatarIDs(int userID);\n\tTask<int> SaveNewAvatar(int userID, byte[] imageData, DateTime timeStamp);\n\tTask DeleteAvatarsByUserID(int userID);\n\tTask<DateTime?> GetLastModificationDate(int userAvatarID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IUserAwardRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IUserAwardRepository\n{\n\tTask IssueAward(int userID, string awardDefinitionID, string title, string description, DateTime timeStamp);\n\tTask<bool> IsAwarded(int userID, string awardDefinitionID);\n\tTask<List<UserAward>> GetAwards(int userID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IUserImageRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IUserImageRepository\n{\n\t[Obsolete(\"Use GetImageStream instead.\")]\n\tTask<byte[]> GetImageData(int userImageID);\n\tTask<IStreamResponse> GetImageStream(int userImageID);\n\tTask<List<UserImage>> GetUserImages(int userID);\n\tTask<int> SaveNewImage(int userID, int sortOrder, bool isApproved, byte[] imageData, DateTime timeStamp);\n\tTask DeleteImagesByUserID(int userID);\n\tTask<DateTime?> GetLastModificationDate(int userImageID);\n\tTask<List<UserImage>> GetUnapprovedUserImages();\n\tTask<bool?> IsUserImageApproved(int userImageID);\n\tTask ApproveUserImage(int userImageID);\n\tTask DeleteUserImage(int userImageID);\n\tTask<UserImage> Get(int userImageID);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IUserRepository.cs",
    "content": "﻿namespace PopForums.Repositories;\n\npublic interface IUserRepository\n{\n\t/// <summary>\n\t/// Stores the hashed password in the data store.\n\t/// </summary>\n\t/// <param name=\"user\">User to update with new hashed password.</param>\n\t/// <param name=\"hashedPassword\">The string of the hashed password.</param>\n\t/// <param name=\"salt\">A Guid that was used in hashing the password.</param>\n\tTask SetHashedPassword(User user, string hashedPassword, Guid salt);\n\n\t/// <summary>\n\t/// Gets the hashed password and salt from the data store of the user whose e-mail is matched.\n\t/// </summary>\n\t/// <param name=\"email\"></param>\n\t/// <returns>A tuple containing the hashed password and salt.</returns>\n\tTask<Tuple<string, Guid?>> GetHashedPasswordByEmail(string email);\n\n\t/// <summary>\n\t/// Gets a user by its ID.\n\t/// </summary>\n\t/// <param name=\"userID\">UserID to match.</param>\n\t/// <returns>Matched user, or null if no matching user is found.</returns>\n\tTask<User> GetUser(int userID);\n\n\t/// <summary>\n\t/// Gets a user by name. Match must be case insensitive.\n\t/// </summary>\n\t/// <param name=\"name\">Name to match.</param>\n\t/// <returns>Matched user, or null if no matching user is found.</returns>\n\tTask<User> GetUserByName(string name);\n\n\t/// <summary>\n\t/// Gets a user by e-mail address. Match must be case insensitive.\n\t/// </summary>\n\t/// <param name=\"email\">E-mail to match.</param>\n\t/// <returns>Matched user, or null if no matching user is found.</returns>\n\tTask<User> GetUserByEmail(string email);\n\n\tTask<User> GetUserByAuthorizationKey(Guid key);\n\n\t/// <summary>\n\t/// Stores new user data and returns a new User object, with new UserID.\n\t/// </summary>\n\t/// <param name=\"name\">Name for new user. Must not be in use.</param>\n\t/// <param name=\"email\">E-mail address for new user. Must not be in use.</param>\n\t/// <param name=\"creationDate\">Date (UTC) to assign as creation date. LastActivityDate and LastLoginDate will be set to this as well.</param>\n\t/// <param name=\"isApproved\">Boolean indicating user approval.</param>\n\t/// <param name=\"hashedPassword\">The hashed password string.</param>\n\t/// <param name=\"authorizationKey\">A Guid used for authorization measures.</param>\n\t/// <param name=\"salt\">A Guid used to salt the password.</param>\n\t/// <returns>A new User object, populated with the data store generated UserID.</returns>\n\tTask<User> CreateUser(string name, string email, DateTime creationDate, bool isApproved, string hashedPassword, Guid authorizationKey, Guid salt);\n\n\t/// <summary>\n\t/// Updates a user record in the data store with a new LastActivityDate.\n\t/// </summary>\n\t/// <param name=\"user\">User to update.</param>\n\t/// <param name=\"newDate\">New value for LastActivityDate value.</param>\n\tTask UpdateLastActivityDate(User user, DateTime newDate);\n\n\t/// <summary>\n\t/// Updates a user record in the data store with a new LastLoginDate.\n\t/// </summary>\n\t/// <param name=\"user\">User to update.</param>\n\t/// <param name=\"newDate\">New value for LastActivityDate value.</param>\n\tTask UpdateLastLoginDate(User user, DateTime newDate);\n\n\t/// <summary>\n\t/// Updates a user record with a new name.\n\t/// </summary>\n\t/// <param name=\"user\">User to update.</param>\n\t/// <param name=\"newName\">New name. Must not be in use by another user, null or empty.</param>\n\tTask ChangeName(User user, string newName);\n\n\t/// <summary>\n\t/// Updates a user record with a new e-mail address.\n\t/// </summary>\n\t/// <param name=\"user\">User to update.</param>\n\t/// <param name=\"newEmail\">New e-mail address. Must not be in use by another user.</param>\n\tTask ChangeEmail(User user, string newEmail);\n\n\tTask UpdateIsApproved(User user, bool isApproved);\n\tTask UpdateAuthorizationKey(User user, Guid key);\n\tTask<List<User>> SearchByEmail(string email);\n\tTask<List<User>> SearchByName(string name);\n\tTask<List<User>> SearchByRole(string role);\n\tTask<List<User>> GetUsersOnline();\n\tTask<List<User>> GetSubscribedUsers();\n\tTask DeleteUser(User user);\n\tTask<List<User>> GetUsersFromIDs(IList<int> ids);\n\tTask<int> GetTotalUsers();\n\tDictionary<int, (User, int)> GetUsersByPointTotals(int top);\n\tTask<List<UserResult>> GetRecentUsers();\n\tTask UpdateTokenExpiration(User user, DateTime? tokenExpiration);\n\tTask UpdateRefreshToken(User user, string refreshToken);\n\tTask<string> GetRefreshToken(User user);\n\tTask<IEnumerable<string>> GetUserNamesThatStartWith(string startingName);\n}"
  },
  {
    "path": "src/PopForums/Repositories/IUserSessionRepository.cs",
    "content": "namespace PopForums.Repositories;\n\npublic interface IUserSessionRepository\n{\n\tTask CreateSession(int sessionID, int? userID, DateTime lastTime);\n\tTask<bool> UpdateSession(int sessionID, DateTime lastTime);\n\tTask<bool> IsSessionAnonymous(int sessionID);\n\tTask<List<ExpiredUserSession>> GetAndDeleteExpiredSessions(DateTime cutOffDate);\n\tTask<ExpiredUserSession> GetSessionIDByUserID(int userID);\n\tTask DeleteSessions(int? userID, int sessionID);\n\tTask<int> GetTotalSessionCount();\n}"
  },
  {
    "path": "src/PopForums/Resources/Resources.Designer.cs",
    "content": "﻿//------------------------------------------------------------------------------\n// <auto-generated>\n//     This code was generated by a tool.\n//\n//     Changes to this file may cause incorrect behavior and will be lost if\n//     the code is regenerated.\n// </auto-generated>\n//------------------------------------------------------------------------------\n\nnamespace PopForums {\n    using System;\n    \n    \n    [System.CodeDom.Compiler.GeneratedCodeAttribute(\"System.Resources.Tools.StronglyTypedResourceBuilder\", \"17.0.0.0\")]\n    [System.Diagnostics.DebuggerNonUserCodeAttribute()]\n    [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]\n    public class Resources {\n        \n        private static System.Resources.ResourceManager resourceMan;\n        \n        private static System.Globalization.CultureInfo resourceCulture;\n        \n        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(\"Microsoft.Performance\", \"CA1811:AvoidUncalledPrivateCode\")]\n        internal Resources() {\n        }\n        \n        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]\n        public static System.Resources.ResourceManager ResourceManager {\n            get {\n                if (object.Equals(null, resourceMan)) {\n                    System.Resources.ResourceManager temp = new System.Resources.ResourceManager(\"PopForums.Resources.Resources\", typeof(Resources).Assembly);\n                    resourceMan = temp;\n                }\n                return resourceMan;\n            }\n        }\n        \n        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]\n        public static System.Globalization.CultureInfo Culture {\n            get {\n                return resourceCulture;\n            }\n            set {\n                resourceCulture = value;\n            }\n        }\n        \n        public static string Account {\n            get {\n                return ResourceManager.GetString(\"Account\", resourceCulture);\n            }\n        }\n        \n        public static string AccountCreated {\n            get {\n                return ResourceManager.GetString(\"AccountCreated\", resourceCulture);\n            }\n        }\n        \n        public static string AccountReady {\n            get {\n                return ResourceManager.GetString(\"AccountReady\", resourceCulture);\n            }\n        }\n        \n        public static string AccountReadyCheckEmail {\n            get {\n                return ResourceManager.GetString(\"AccountReadyCheckEmail\", resourceCulture);\n            }\n        }\n        \n        public static string AccountVerified {\n            get {\n                return ResourceManager.GetString(\"AccountVerified\", resourceCulture);\n            }\n        }\n        \n        public static string AlreadyCreatedAccount {\n            get {\n                return ResourceManager.GetString(\"AlreadyCreatedAccount\", resourceCulture);\n            }\n        }\n        \n        public static string AsYouWouldLikeItToAppear {\n            get {\n                return ResourceManager.GetString(\"AsYouWouldLikeItToAppear\", resourceCulture);\n            }\n        }\n        \n        public static string AtLeastSixChar {\n            get {\n                return ResourceManager.GetString(\"AtLeastSixChar\", resourceCulture);\n            }\n        }\n        \n        public static string By {\n            get {\n                return ResourceManager.GetString(\"By\", resourceCulture);\n            }\n        }\n        \n        public static string CreateAccountButton {\n            get {\n                return ResourceManager.GetString(\"CreateAccountButton\", resourceCulture);\n            }\n        }\n        \n        public static string CreateAnAccount {\n            get {\n                return ResourceManager.GetString(\"CreateAnAccount\", resourceCulture);\n            }\n        }\n        \n        public static string EditYourProfile {\n            get {\n                return ResourceManager.GetString(\"EditYourProfile\", resourceCulture);\n            }\n        }\n        \n        public static string Email {\n            get {\n                return ResourceManager.GetString(\"Email\", resourceCulture);\n            }\n        }\n        \n        public static string EmailProblemAccount {\n            get {\n                return ResourceManager.GetString(\"EmailProblemAccount\", resourceCulture);\n            }\n        }\n        \n        public static string Favorites {\n            get {\n                return ResourceManager.GetString(\"Favorites\", resourceCulture);\n            }\n        }\n        \n        public static string Forbidden {\n            get {\n                return ResourceManager.GetString(\"Forbidden\", resourceCulture);\n            }\n        }\n        \n        public static string Forums {\n            get {\n                return ResourceManager.GetString(\"Forums\", resourceCulture);\n            }\n        }\n        \n        public static string HaveReadTOS {\n            get {\n                return ResourceManager.GetString(\"HaveReadTOS\", resourceCulture);\n            }\n        }\n        \n        public static string Last {\n            get {\n                return ResourceManager.GetString(\"Last\", resourceCulture);\n            }\n        }\n        \n        public static string Login {\n            get {\n                return ResourceManager.GetString(\"Login\", resourceCulture);\n            }\n        }\n        \n        public static string Logout {\n            get {\n                return ResourceManager.GetString(\"Logout\", resourceCulture);\n            }\n        }\n        \n        public static string MarkAllForumsRead {\n            get {\n                return ResourceManager.GetString(\"MarkAllForumsRead\", resourceCulture);\n            }\n        }\n        \n        public static string MyBirthdayIsOnOrBefore {\n            get {\n                return ResourceManager.GetString(\"MyBirthdayIsOnOrBefore\", resourceCulture);\n            }\n        }\n        \n        public static string Name {\n            get {\n                return ResourceManager.GetString(\"Name\", resourceCulture);\n            }\n        }\n        \n        public static string NeedToVerifyExistingAccount {\n            get {\n                return ResourceManager.GetString(\"NeedToVerifyExistingAccount\", resourceCulture);\n            }\n        }\n        \n        public static string PageNotFound {\n            get {\n                return ResourceManager.GetString(\"PageNotFound\", resourceCulture);\n            }\n        }\n        \n        public static string Password {\n            get {\n                return ResourceManager.GetString(\"Password\", resourceCulture);\n            }\n        }\n        \n        public static string Posts {\n            get {\n                return ResourceManager.GetString(\"Posts\", resourceCulture);\n            }\n        }\n        \n        public static string PrivateMessages {\n            get {\n                return ResourceManager.GetString(\"PrivateMessages\", resourceCulture);\n            }\n        }\n        \n        public static string Recent {\n            get {\n                return ResourceManager.GetString(\"Recent\", resourceCulture);\n            }\n        }\n        \n        public static string RegisteredUsers {\n            get {\n                return ResourceManager.GetString(\"RegisteredUsers\", resourceCulture);\n            }\n        }\n        \n        public static string RetypePassword {\n            get {\n                return ResourceManager.GetString(\"RetypePassword\", resourceCulture);\n            }\n        }\n        \n        public static string Search {\n            get {\n                return ResourceManager.GetString(\"Search\", resourceCulture);\n            }\n        }\n        \n        public static string SubscribeToList {\n            get {\n                return ResourceManager.GetString(\"SubscribeToList\", resourceCulture);\n            }\n        }\n        \n        public static string Subscriptions {\n            get {\n                return ResourceManager.GetString(\"Subscriptions\", resourceCulture);\n            }\n        }\n        \n        public static string TermsOfService {\n            get {\n                return ResourceManager.GetString(\"TermsOfService\", resourceCulture);\n            }\n        }\n        \n        public static string TimeZone {\n            get {\n                return ResourceManager.GetString(\"TimeZone\", resourceCulture);\n            }\n        }\n        \n        public static string Topics {\n            get {\n                return ResourceManager.GetString(\"Topics\", resourceCulture);\n            }\n        }\n        \n        public static string Total {\n            get {\n                return ResourceManager.GetString(\"Total\", resourceCulture);\n            }\n        }\n        \n        public static string TotalPosts {\n            get {\n                return ResourceManager.GetString(\"TotalPosts\", resourceCulture);\n            }\n        }\n        \n        public static string TotalTopics {\n            get {\n                return ResourceManager.GetString(\"TotalTopics\", resourceCulture);\n            }\n        }\n        \n        public static string UseDaylight {\n            get {\n                return ResourceManager.GetString(\"UseDaylight\", resourceCulture);\n            }\n        }\n        \n        public static string UsersOnline {\n            get {\n                return ResourceManager.GetString(\"UsersOnline\", resourceCulture);\n            }\n        }\n        \n        public static string MustBe13 {\n            get {\n                return ResourceManager.GetString(\"MustBe13\", resourceCulture);\n            }\n        }\n        \n        public static string MustAcceptTOS {\n            get {\n                return ResourceManager.GetString(\"MustAcceptTOS\", resourceCulture);\n            }\n        }\n        \n        public static string RetypeYourPassword {\n            get {\n                return ResourceManager.GetString(\"RetypeYourPassword\", resourceCulture);\n            }\n        }\n        \n        public static string NameRequired {\n            get {\n                return ResourceManager.GetString(\"NameRequired\", resourceCulture);\n            }\n        }\n        \n        public static string NameInUse {\n            get {\n                return ResourceManager.GetString(\"NameInUse\", resourceCulture);\n            }\n        }\n        \n        public static string EmailRequired {\n            get {\n                return ResourceManager.GetString(\"EmailRequired\", resourceCulture);\n            }\n        }\n        \n        public static string ValidEmailAddressRequired {\n            get {\n                return ResourceManager.GetString(\"ValidEmailAddressRequired\", resourceCulture);\n            }\n        }\n        \n        public static string EmailInUse {\n            get {\n                return ResourceManager.GetString(\"EmailInUse\", resourceCulture);\n            }\n        }\n        \n        public static string EmailBanned {\n            get {\n                return ResourceManager.GetString(\"EmailBanned\", resourceCulture);\n            }\n        }\n        \n        public static string IPBanned {\n            get {\n                return ResourceManager.GetString(\"IPBanned\", resourceCulture);\n            }\n        }\n        \n        public static string RetypePasswordMustMatch {\n            get {\n                return ResourceManager.GetString(\"RetypePasswordMustMatch\", resourceCulture);\n            }\n        }\n        \n        public static string EditAccount {\n            get {\n                return ResourceManager.GetString(\"EditAccount\", resourceCulture);\n            }\n        }\n        \n        public static string EditProfile {\n            get {\n                return ResourceManager.GetString(\"EditProfile\", resourceCulture);\n            }\n        }\n        \n        public static string EnterVerificationCode {\n            get {\n                return ResourceManager.GetString(\"EnterVerificationCode\", resourceCulture);\n            }\n        }\n        \n        public static string ForumHomePage {\n            get {\n                return ResourceManager.GetString(\"ForumHomePage\", resourceCulture);\n            }\n        }\n        \n        public static string MustBeRegisteredToEditAccount {\n            get {\n                return ResourceManager.GetString(\"MustBeRegisteredToEditAccount\", resourceCulture);\n            }\n        }\n        \n        public static string NoUserFoundWithEmail {\n            get {\n                return ResourceManager.GetString(\"NoUserFoundWithEmail\", resourceCulture);\n            }\n        }\n        \n        public static string SendEmailWithNewCodeButton {\n            get {\n                return ResourceManager.GetString(\"SendEmailWithNewCodeButton\", resourceCulture);\n            }\n        }\n        \n        public static string VerificationEmailSent {\n            get {\n                return ResourceManager.GetString(\"VerificationEmailSent\", resourceCulture);\n            }\n        }\n        \n        public static string VerificationFailure {\n            get {\n                return ResourceManager.GetString(\"VerificationFailure\", resourceCulture);\n            }\n        }\n        \n        public static string VerificationIfYouNeed {\n            get {\n                return ResourceManager.GetString(\"VerificationIfYouNeed\", resourceCulture);\n            }\n        }\n        \n        public static string VerificationLinkBad {\n            get {\n                return ResourceManager.GetString(\"VerificationLinkBad\", resourceCulture);\n            }\n        }\n        \n        public static string VerifyAccount {\n            get {\n                return ResourceManager.GetString(\"VerifyAccount\", resourceCulture);\n            }\n        }\n        \n        public static string VerifyCodeButton {\n            get {\n                return ResourceManager.GetString(\"VerifyCodeButton\", resourceCulture);\n            }\n        }\n        \n        public static string Avatar {\n            get {\n                return ResourceManager.GetString(\"Avatar\", resourceCulture);\n            }\n        }\n        \n        public static string AvatarDelete {\n            get {\n                return ResourceManager.GetString(\"AvatarDelete\", resourceCulture);\n            }\n        }\n        \n        public static string ChangeEmail {\n            get {\n                return ResourceManager.GetString(\"ChangeEmail\", resourceCulture);\n            }\n        }\n        \n        public static string ChangeEmailButton {\n            get {\n                return ResourceManager.GetString(\"ChangeEmailButton\", resourceCulture);\n            }\n        }\n        \n        public static string ChangeEmailConsequence {\n            get {\n                return ResourceManager.GetString(\"ChangeEmailConsequence\", resourceCulture);\n            }\n        }\n        \n        public static string ChangePassword {\n            get {\n                return ResourceManager.GetString(\"ChangePassword\", resourceCulture);\n            }\n        }\n        \n        public static string ChangeYourEmailPassword {\n            get {\n                return ResourceManager.GetString(\"ChangeYourEmailPassword\", resourceCulture);\n            }\n        }\n        \n        public static string DateOfBirth {\n            get {\n                return ResourceManager.GetString(\"DateOfBirth\", resourceCulture);\n            }\n        }\n        \n        public static string Details {\n            get {\n                return ResourceManager.GetString(\"Details\", resourceCulture);\n            }\n        }\n        \n        public static string EmailChangeSuccess {\n            get {\n                return ResourceManager.GetString(\"EmailChangeSuccess\", resourceCulture);\n            }\n        }\n        \n        public static string EmailNew {\n            get {\n                return ResourceManager.GetString(\"EmailNew\", resourceCulture);\n            }\n        }\n        \n        public static string EmailNewRetype {\n            get {\n                return ResourceManager.GetString(\"EmailNewRetype\", resourceCulture);\n            }\n        }\n        \n        public static string EmailNotFound {\n            get {\n                return ResourceManager.GetString(\"EmailNotFound\", resourceCulture);\n            }\n        }\n        \n        public static string EmailSent {\n            get {\n                return ResourceManager.GetString(\"EmailSent\", resourceCulture);\n            }\n        }\n        \n        public static string EmailsMustMatch {\n            get {\n                return ResourceManager.GetString(\"EmailsMustMatch\", resourceCulture);\n            }\n        }\n        \n        public static string EmailUser {\n            get {\n                return ResourceManager.GetString(\"EmailUser\", resourceCulture);\n            }\n        }\n        \n        public static string ForcePlainTextBox {\n            get {\n                return ResourceManager.GetString(\"ForcePlainTextBox\", resourceCulture);\n            }\n        }\n        \n        public static string ForgotInstructions {\n            get {\n                return ResourceManager.GetString(\"ForgotInstructions\", resourceCulture);\n            }\n        }\n        \n        public static string ForgotInstructionsSent {\n            get {\n                return ResourceManager.GetString(\"ForgotInstructionsSent\", resourceCulture);\n            }\n        }\n        \n        public static string ForgotPassword {\n            get {\n                return ResourceManager.GetString(\"ForgotPassword\", resourceCulture);\n            }\n        }\n        \n        public static string ForgotPasswordQuestion {\n            get {\n                return ResourceManager.GetString(\"ForgotPasswordQuestion\", resourceCulture);\n            }\n        }\n        \n        public static string HideVanity {\n            get {\n                return ResourceManager.GetString(\"HideVanity\", resourceCulture);\n            }\n        }\n        \n        public static string In {\n            get {\n                return ResourceManager.GetString(\"In\", resourceCulture);\n            }\n        }\n        \n        public static string Joined {\n            get {\n                return ResourceManager.GetString(\"Joined\", resourceCulture);\n            }\n        }\n        \n        public static string Location {\n            get {\n                return ResourceManager.GetString(\"Location\", resourceCulture);\n            }\n        }\n        \n        public static string LoginAlready {\n            get {\n                return ResourceManager.GetString(\"LoginAlready\", resourceCulture);\n            }\n        }\n        \n        public static string LoginBad {\n            get {\n                return ResourceManager.GetString(\"LoginBad\", resourceCulture);\n            }\n        }\n        \n        public static string ManagePhotos {\n            get {\n                return ResourceManager.GetString(\"ManagePhotos\", resourceCulture);\n            }\n        }\n        \n        public static string ManageYourPhotos {\n            get {\n                return ResourceManager.GetString(\"ManageYourPhotos\", resourceCulture);\n            }\n        }\n        \n        public static string NewPasswordSaved {\n            get {\n                return ResourceManager.GetString(\"NewPasswordSaved\", resourceCulture);\n            }\n        }\n        \n        public static string NotRegisteredQuestion {\n            get {\n                return ResourceManager.GetString(\"NotRegisteredQuestion\", resourceCulture);\n            }\n        }\n        \n        public static string OldPasswordIncorrect {\n            get {\n                return ResourceManager.GetString(\"OldPasswordIncorrect\", resourceCulture);\n            }\n        }\n        \n        public static string Options {\n            get {\n                return ResourceManager.GetString(\"Options\", resourceCulture);\n            }\n        }\n        \n        public static string PasswordNew {\n            get {\n                return ResourceManager.GetString(\"PasswordNew\", resourceCulture);\n            }\n        }\n        \n        public static string PasswordNewRetype {\n            get {\n                return ResourceManager.GetString(\"PasswordNewRetype\", resourceCulture);\n            }\n        }\n        \n        public static string PasswordOld {\n            get {\n                return ResourceManager.GetString(\"PasswordOld\", resourceCulture);\n            }\n        }\n        \n        public static string PasswordReset {\n            get {\n                return ResourceManager.GetString(\"PasswordReset\", resourceCulture);\n            }\n        }\n        \n        public static string PasswordResetLinkInvalid {\n            get {\n                return ResourceManager.GetString(\"PasswordResetLinkInvalid\", resourceCulture);\n            }\n        }\n        \n        public static string PasswordResetNote {\n            get {\n                return ResourceManager.GetString(\"PasswordResetNote\", resourceCulture);\n            }\n        }\n        \n        public static string PasswordResetSuccess {\n            get {\n                return ResourceManager.GetString(\"PasswordResetSuccess\", resourceCulture);\n            }\n        }\n        \n        public static string Photo {\n            get {\n                return ResourceManager.GetString(\"Photo\", resourceCulture);\n            }\n        }\n        \n        public static string PhotoDelete {\n            get {\n                return ResourceManager.GetString(\"PhotoDelete\", resourceCulture);\n            }\n        }\n        \n        public static string PhotoNotApproved {\n            get {\n                return ResourceManager.GetString(\"PhotoNotApproved\", resourceCulture);\n            }\n        }\n        \n        public static string ProfileUpdated {\n            get {\n                return ResourceManager.GetString(\"ProfileUpdated\", resourceCulture);\n            }\n        }\n        \n        public static string RememberMe {\n            get {\n                return ResourceManager.GetString(\"RememberMe\", resourceCulture);\n            }\n        }\n        \n        public static string Replies {\n            get {\n                return ResourceManager.GetString(\"Replies\", resourceCulture);\n            }\n        }\n        \n        public static string Save {\n            get {\n                return ResourceManager.GetString(\"Save\", resourceCulture);\n            }\n        }\n        \n        public static string SendEmailButton {\n            get {\n                return ResourceManager.GetString(\"SendEmailButton\", resourceCulture);\n            }\n        }\n        \n        public static string SendPM {\n            get {\n                return ResourceManager.GetString(\"SendPM\", resourceCulture);\n            }\n        }\n        \n        public static string ShowProfileDetails {\n            get {\n                return ResourceManager.GetString(\"ShowProfileDetails\", resourceCulture);\n            }\n        }\n        \n        public static string Signature {\n            get {\n                return ResourceManager.GetString(\"Signature\", resourceCulture);\n            }\n        }\n        \n        public static string StartedBy {\n            get {\n                return ResourceManager.GetString(\"StartedBy\", resourceCulture);\n            }\n        }\n        \n        public static string Subject {\n            get {\n                return ResourceManager.GetString(\"Subject\", resourceCulture);\n            }\n        }\n        \n        public static string Text {\n            get {\n                return ResourceManager.GetString(\"Text\", resourceCulture);\n            }\n        }\n        \n        public static string To {\n            get {\n                return ResourceManager.GetString(\"To\", resourceCulture);\n            }\n        }\n        \n        public static string UploadNew {\n            get {\n                return ResourceManager.GetString(\"UploadNew\", resourceCulture);\n            }\n        }\n        \n        public static string UserNotFound {\n            get {\n                return ResourceManager.GetString(\"UserNotFound\", resourceCulture);\n            }\n        }\n        \n        public static string Views {\n            get {\n                return ResourceManager.GetString(\"Views\", resourceCulture);\n            }\n        }\n        \n        public static string Web {\n            get {\n                return ResourceManager.GetString(\"Web\", resourceCulture);\n            }\n        }\n        \n        public static string WebVisit {\n            get {\n                return ResourceManager.GetString(\"WebVisit\", resourceCulture);\n            }\n        }\n        \n        public static string YourIP {\n            get {\n                return ResourceManager.GetString(\"YourIP\", resourceCulture);\n            }\n        }\n        \n        public static string Birthday {\n            get {\n                return ResourceManager.GetString(\"Birthday\", resourceCulture);\n            }\n        }\n        \n        public static string Contact {\n            get {\n                return ResourceManager.GetString(\"Contact\", resourceCulture);\n            }\n        }\n        \n        public static string Profile {\n            get {\n                return ResourceManager.GetString(\"Profile\", resourceCulture);\n            }\n        }\n        \n        public static string SendNameEmail {\n            get {\n                return ResourceManager.GetString(\"SendNameEmail\", resourceCulture);\n            }\n        }\n        \n        public static string SendNamePM {\n            get {\n                return ResourceManager.GetString(\"SendNamePM\", resourceCulture);\n            }\n        }\n        \n        public static string Unsubscribe {\n            get {\n                return ResourceManager.GetString(\"Unsubscribe\", resourceCulture);\n            }\n        }\n        \n        public static string UnsubscribeFail {\n            get {\n                return ResourceManager.GetString(\"UnsubscribeFail\", resourceCulture);\n            }\n        }\n        \n        public static string UnsubscribeLinkBad {\n            get {\n                return ResourceManager.GetString(\"UnsubscribeLinkBad\", resourceCulture);\n            }\n        }\n        \n        public static string UnsubscribeNote {\n            get {\n                return ResourceManager.GetString(\"UnsubscribeNote\", resourceCulture);\n            }\n        }\n        \n        public static string AddForum {\n            get {\n                return ResourceManager.GetString(\"AddForum\", resourceCulture);\n            }\n        }\n        \n        public static string AddNew {\n            get {\n                return ResourceManager.GetString(\"AddNew\", resourceCulture);\n            }\n        }\n        \n        public static string AllowImages {\n            get {\n                return ResourceManager.GetString(\"AllowImages\", resourceCulture);\n            }\n        }\n        \n        public static string Archived {\n            get {\n                return ResourceManager.GetString(\"Archived\", resourceCulture);\n            }\n        }\n        \n        public static string AreYouSure {\n            get {\n                return ResourceManager.GetString(\"AreYouSure\", resourceCulture);\n            }\n        }\n        \n        public static string Ban {\n            get {\n                return ResourceManager.GetString(\"Ban\", resourceCulture);\n            }\n        }\n        \n        public static string BanRemove {\n            get {\n                return ResourceManager.GetString(\"BanRemove\", resourceCulture);\n            }\n        }\n        \n        public static string Categories {\n            get {\n                return ResourceManager.GetString(\"Categories\", resourceCulture);\n            }\n        }\n        \n        public static string Category {\n            get {\n                return ResourceManager.GetString(\"Category\", resourceCulture);\n            }\n        }\n        \n        public static string CategoryEditTitle {\n            get {\n                return ResourceManager.GetString(\"CategoryEditTitle\", resourceCulture);\n            }\n        }\n        \n        public static string CategoryMoveError {\n            get {\n                return ResourceManager.GetString(\"CategoryMoveError\", resourceCulture);\n            }\n        }\n        \n        public static string CensorReplacementChar {\n            get {\n                return ResourceManager.GetString(\"CensorReplacementChar\", resourceCulture);\n            }\n        }\n        \n        public static string CensorWords {\n            get {\n                return ResourceManager.GetString(\"CensorWords\", resourceCulture);\n            }\n        }\n        \n        public static string Delete {\n            get {\n                return ResourceManager.GetString(\"Delete\", resourceCulture);\n            }\n        }\n        \n        public static string DeleteAndBanButton {\n            get {\n                return ResourceManager.GetString(\"DeleteAndBanButton\", resourceCulture);\n            }\n        }\n        \n        public static string DeleteUserButton {\n            get {\n                return ResourceManager.GetString(\"DeleteUserButton\", resourceCulture);\n            }\n        }\n        \n        public static string Description {\n            get {\n                return ResourceManager.GetString(\"Description\", resourceCulture);\n            }\n        }\n        \n        public static string Down {\n            get {\n                return ResourceManager.GetString(\"Down\", resourceCulture);\n            }\n        }\n        \n        public static string Edit {\n            get {\n                return ResourceManager.GetString(\"Edit\", resourceCulture);\n            }\n        }\n        \n        public static string EditForum {\n            get {\n                return ResourceManager.GetString(\"EditForum\", resourceCulture);\n            }\n        }\n        \n        public static string EditUser {\n            get {\n                return ResourceManager.GetString(\"EditUser\", resourceCulture);\n            }\n        }\n        \n        public static string EmailBan {\n            get {\n                return ResourceManager.GetString(\"EmailBan\", resourceCulture);\n            }\n        }\n        \n        public static string EmailIpBan {\n            get {\n                return ResourceManager.GetString(\"EmailIpBan\", resourceCulture);\n            }\n        }\n        \n        public static string EmailNewOptional {\n            get {\n                return ResourceManager.GetString(\"EmailNewOptional\", resourceCulture);\n            }\n        }\n        \n        public static string EmailUsers {\n            get {\n                return ResourceManager.GetString(\"EmailUsers\", resourceCulture);\n            }\n        }\n        \n        public static string ErrorLog {\n            get {\n                return ResourceManager.GetString(\"ErrorLog\", resourceCulture);\n            }\n        }\n        \n        public static string Forum {\n            get {\n                return ResourceManager.GetString(\"Forum\", resourceCulture);\n            }\n        }\n        \n        public static string ForumAdapter {\n            get {\n                return ResourceManager.GetString(\"ForumAdapter\", resourceCulture);\n            }\n        }\n        \n        public static string ForumHome {\n            get {\n                return ResourceManager.GetString(\"ForumHome\", resourceCulture);\n            }\n        }\n        \n        public static string ForumPermisions {\n            get {\n                return ResourceManager.GetString(\"ForumPermisions\", resourceCulture);\n            }\n        }\n        \n        public static string ForumSettings {\n            get {\n                return ResourceManager.GetString(\"ForumSettings\", resourceCulture);\n            }\n        }\n        \n        public static string ForumsUncat {\n            get {\n                return ResourceManager.GetString(\"ForumsUncat\", resourceCulture);\n            }\n        }\n        \n        public static string GeneralSettings {\n            get {\n                return ResourceManager.GetString(\"GeneralSettings\", resourceCulture);\n            }\n        }\n        \n        public static string IpBan {\n            get {\n                return ResourceManager.GetString(\"IpBan\", resourceCulture);\n            }\n        }\n        \n        public static string IpHistory {\n            get {\n                return ResourceManager.GetString(\"IpHistory\", resourceCulture);\n            }\n        }\n        \n        public static string IsApproved {\n            get {\n                return ResourceManager.GetString(\"IsApproved\", resourceCulture);\n            }\n        }\n        \n        public static string IsSubscribed {\n            get {\n                return ResourceManager.GetString(\"IsSubscribed\", resourceCulture);\n            }\n        }\n        \n        public static string LogErrors {\n            get {\n                return ResourceManager.GetString(\"LogErrors\", resourceCulture);\n            }\n        }\n        \n        public static string Logging {\n            get {\n                return ResourceManager.GetString(\"Logging\", resourceCulture);\n            }\n        }\n        \n        public static string LogMod {\n            get {\n                return ResourceManager.GetString(\"LogMod\", resourceCulture);\n            }\n        }\n        \n        public static string LogSecurity {\n            get {\n                return ResourceManager.GetString(\"LogSecurity\", resourceCulture);\n            }\n        }\n        \n        public static string MinimumTimeBetweenPosts {\n            get {\n                return ResourceManager.GetString(\"MinimumTimeBetweenPosts\", resourceCulture);\n            }\n        }\n        \n        public static string ModerationLog {\n            get {\n                return ResourceManager.GetString(\"ModerationLog\", resourceCulture);\n            }\n        }\n        \n        public static string Move {\n            get {\n                return ResourceManager.GetString(\"Move\", resourceCulture);\n            }\n        }\n        \n        public static string NewUserApprovedWithoutVerification {\n            get {\n                return ResourceManager.GetString(\"NewUserApprovedWithoutVerification\", resourceCulture);\n            }\n        }\n        \n        public static string NewUserImageApprovedWithoutMod {\n            get {\n                return ResourceManager.GetString(\"NewUserImageApprovedWithoutMod\", resourceCulture);\n            }\n        }\n        \n        public static string Parsing {\n            get {\n                return ResourceManager.GetString(\"Parsing\", resourceCulture);\n            }\n        }\n        \n        public static string PasswordNewOptional {\n            get {\n                return ResourceManager.GetString(\"PasswordNewOptional\", resourceCulture);\n            }\n        }\n        \n        public static string PopForumsAdmin {\n            get {\n                return ResourceManager.GetString(\"PopForumsAdmin\", resourceCulture);\n            }\n        }\n        \n        public static string PostsPerPage {\n            get {\n                return ResourceManager.GetString(\"PostsPerPage\", resourceCulture);\n            }\n        }\n        \n        public static string Role {\n            get {\n                return ResourceManager.GetString(\"Role\", resourceCulture);\n            }\n        }\n        \n        public static string Roles {\n            get {\n                return ResourceManager.GetString(\"Roles\", resourceCulture);\n            }\n        }\n        \n        public static string Security {\n            get {\n                return ResourceManager.GetString(\"Security\", resourceCulture);\n            }\n        }\n        \n        public static string SecurityLog {\n            get {\n                return ResourceManager.GetString(\"SecurityLog\", resourceCulture);\n            }\n        }\n        \n        public static string ServerTime {\n            get {\n                return ResourceManager.GetString(\"ServerTime\", resourceCulture);\n            }\n        }\n        \n        public static string Services {\n            get {\n                return ResourceManager.GetString(\"Services\", resourceCulture);\n            }\n        }\n        \n        public static string SessionLength {\n            get {\n                return ResourceManager.GetString(\"SessionLength\", resourceCulture);\n            }\n        }\n        \n        public static string Size {\n            get {\n                return ResourceManager.GetString(\"Size\", resourceCulture);\n            }\n        }\n        \n        public static string SubjectAndBodyNotEmpty {\n            get {\n                return ResourceManager.GetString(\"SubjectAndBodyNotEmpty\", resourceCulture);\n            }\n        }\n        \n        public static string Title {\n            get {\n                return ResourceManager.GetString(\"Title\", resourceCulture);\n            }\n        }\n        \n        public static string TopicsPerPage {\n            get {\n                return ResourceManager.GetString(\"TopicsPerPage\", resourceCulture);\n            }\n        }\n        \n        public static string Up {\n            get {\n                return ResourceManager.GetString(\"Up\", resourceCulture);\n            }\n        }\n        \n        public static string UserAvatarMaxDim {\n            get {\n                return ResourceManager.GetString(\"UserAvatarMaxDim\", resourceCulture);\n            }\n        }\n        \n        public static string UserDeleteWarning {\n            get {\n                return ResourceManager.GetString(\"UserDeleteWarning\", resourceCulture);\n            }\n        }\n        \n        public static string UserImageApproval {\n            get {\n                return ResourceManager.GetString(\"UserImageApproval\", resourceCulture);\n            }\n        }\n        \n        public static string UserImageMaxDim {\n            get {\n                return ResourceManager.GetString(\"UserImageMaxDim\", resourceCulture);\n            }\n        }\n        \n        public static string UserRoles {\n            get {\n                return ResourceManager.GetString(\"UserRoles\", resourceCulture);\n            }\n        }\n        \n        public static string Visible {\n            get {\n                return ResourceManager.GetString(\"Visible\", resourceCulture);\n            }\n        }\n        \n        public static string AddNewForum {\n            get {\n                return ResourceManager.GetString(\"AddNewForum\", resourceCulture);\n            }\n        }\n        \n        public static string Body {\n            get {\n                return ResourceManager.GetString(\"Body\", resourceCulture);\n            }\n        }\n        \n        public static string BodyHtml {\n            get {\n                return ResourceManager.GetString(\"BodyHtml\", resourceCulture);\n            }\n        }\n        \n        public static string ClickToLoadMorePosts {\n            get {\n                return ResourceManager.GetString(\"ClickToLoadMorePosts\", resourceCulture);\n            }\n        }\n        \n        public static string Comment {\n            get {\n                return ResourceManager.GetString(\"Comment\", resourceCulture);\n            }\n        }\n        \n        public static string CommentsOptional {\n            get {\n                return ResourceManager.GetString(\"CommentsOptional\", resourceCulture);\n            }\n        }\n        \n        public static string CreateJunkWordButton {\n            get {\n                return ResourceManager.GetString(\"CreateJunkWordButton\", resourceCulture);\n            }\n        }\n        \n        public static string CreateNewReply {\n            get {\n                return ResourceManager.GetString(\"CreateNewReply\", resourceCulture);\n            }\n        }\n        \n        public static string CreateNewRole {\n            get {\n                return ResourceManager.GetString(\"CreateNewRole\", resourceCulture);\n            }\n        }\n        \n        public static string CreateNewTopic {\n            get {\n                return ResourceManager.GetString(\"CreateNewTopic\", resourceCulture);\n            }\n        }\n        \n        public static string DeleteAllErrors {\n            get {\n                return ResourceManager.GetString(\"DeleteAllErrors\", resourceCulture);\n            }\n        }\n        \n        public static string DeleteJunkWordButton {\n            get {\n                return ResourceManager.GetString(\"DeleteJunkWordButton\", resourceCulture);\n            }\n        }\n        \n        public static string DeleteSelectedRole {\n            get {\n                return ResourceManager.GetString(\"DeleteSelectedRole\", resourceCulture);\n            }\n        }\n        \n        public static string EditPost {\n            get {\n                return ResourceManager.GetString(\"EditPost\", resourceCulture);\n            }\n        }\n        \n        public static string EmailUsersQueued {\n            get {\n                return ResourceManager.GetString(\"EmailUsersQueued\", resourceCulture);\n            }\n        }\n        \n        public static string EndDate {\n            get {\n                return ResourceManager.GetString(\"EndDate\", resourceCulture);\n            }\n        }\n        \n        public static string Event {\n            get {\n                return ResourceManager.GetString(\"Event\", resourceCulture);\n            }\n        }\n        \n        public static string EventTime {\n            get {\n                return ResourceManager.GetString(\"EventTime\", resourceCulture);\n            }\n        }\n        \n        public static string FavoriteMustBeLoggedIn {\n            get {\n                return ResourceManager.GetString(\"FavoriteMustBeLoggedIn\", resourceCulture);\n            }\n        }\n        \n        public static string FavoritesDontHave {\n            get {\n                return ResourceManager.GetString(\"FavoritesDontHave\", resourceCulture);\n            }\n        }\n        \n        public static string FavoriteTopics {\n            get {\n                return ResourceManager.GetString(\"FavoriteTopics\", resourceCulture);\n            }\n        }\n        \n        public static string ForumMoveError {\n            get {\n                return ResourceManager.GetString(\"ForumMoveError\", resourceCulture);\n            }\n        }\n        \n        public static string ForumPermissionInstructions {\n            get {\n                return ResourceManager.GetString(\"ForumPermissionInstructions\", resourceCulture);\n            }\n        }\n        \n        public static string FromEmailAddress {\n            get {\n                return ResourceManager.GetString(\"FromEmailAddress\", resourceCulture);\n            }\n        }\n        \n        public static string ID {\n            get {\n                return ResourceManager.GetString(\"ID\", resourceCulture);\n            }\n        }\n        \n        public static string IncludeSignature {\n            get {\n                return ResourceManager.GetString(\"IncludeSignature\", resourceCulture);\n            }\n        }\n        \n        public static string IndexingInterval {\n            get {\n                return ResourceManager.GetString(\"IndexingInterval\", resourceCulture);\n            }\n        }\n        \n        public static string IP {\n            get {\n                return ResourceManager.GetString(\"IP\", resourceCulture);\n            }\n        }\n        \n        public static string IsRunning {\n            get {\n                return ResourceManager.GetString(\"IsRunning\", resourceCulture);\n            }\n        }\n        \n        public static string JunkWords {\n            get {\n                return ResourceManager.GetString(\"JunkWords\", resourceCulture);\n            }\n        }\n        \n        public static string MarkForumRead {\n            get {\n                return ResourceManager.GetString(\"MarkForumRead\", resourceCulture);\n            }\n        }\n        \n        public static string Message {\n            get {\n                return ResourceManager.GetString(\"Message\", resourceCulture);\n            }\n        }\n        \n        public static string Millseconds {\n            get {\n                return ResourceManager.GetString(\"Millseconds\", resourceCulture);\n            }\n        }\n        \n        public static string NamePosts {\n            get {\n                return ResourceManager.GetString(\"NamePosts\", resourceCulture);\n            }\n        }\n        \n        public static string Optional {\n            get {\n                return ResourceManager.GetString(\"Optional\", resourceCulture);\n            }\n        }\n        \n        public static string PermRoles {\n            get {\n                return ResourceManager.GetString(\"PermRoles\", resourceCulture);\n            }\n        }\n        \n        public static string PostID {\n            get {\n                return ResourceManager.GetString(\"PostID\", resourceCulture);\n            }\n        }\n        \n        public static string PostingRoles {\n            get {\n                return ResourceManager.GetString(\"PostingRoles\", resourceCulture);\n            }\n        }\n        \n        public static string PostNewTopic {\n            get {\n                return ResourceManager.GetString(\"PostNewTopic\", resourceCulture);\n            }\n        }\n        \n        public static string PreviewTopic {\n            get {\n                return ResourceManager.GetString(\"PreviewTopic\", resourceCulture);\n            }\n        }\n        \n        public static string Remove {\n            get {\n                return ResourceManager.GetString(\"Remove\", resourceCulture);\n            }\n        }\n        \n        public static string RemoveAll {\n            get {\n                return ResourceManager.GetString(\"RemoveAll\", resourceCulture);\n            }\n        }\n        \n        public static string SendingInterval {\n            get {\n                return ResourceManager.GetString(\"SendingInterval\", resourceCulture);\n            }\n        }\n        \n        public static string SmtpPassword {\n            get {\n                return ResourceManager.GetString(\"SmtpPassword\", resourceCulture);\n            }\n        }\n        \n        public static string SmtpPort {\n            get {\n                return ResourceManager.GetString(\"SmtpPort\", resourceCulture);\n            }\n        }\n        \n        public static string SmtpServer {\n            get {\n                return ResourceManager.GetString(\"SmtpServer\", resourceCulture);\n            }\n        }\n        \n        public static string SmtpUser {\n            get {\n                return ResourceManager.GetString(\"SmtpUser\", resourceCulture);\n            }\n        }\n        \n        public static string StartDate {\n            get {\n                return ResourceManager.GetString(\"StartDate\", resourceCulture);\n            }\n        }\n        \n        public static string SubmitNewTopic {\n            get {\n                return ResourceManager.GetString(\"SubmitNewTopic\", resourceCulture);\n            }\n        }\n        \n        public static string SubmitReply {\n            get {\n                return ResourceManager.GetString(\"SubmitReply\", resourceCulture);\n            }\n        }\n        \n        public static string TopicID {\n            get {\n                return ResourceManager.GetString(\"TopicID\", resourceCulture);\n            }\n        }\n        \n        public static string Type {\n            get {\n                return ResourceManager.GetString(\"Type\", resourceCulture);\n            }\n        }\n        \n        public static string UseEsmtpCred {\n            get {\n                return ResourceManager.GetString(\"UseEsmtpCred\", resourceCulture);\n            }\n        }\n        \n        public static string UserID {\n            get {\n                return ResourceManager.GetString(\"UserID\", resourceCulture);\n            }\n        }\n        \n        public static string UserIDTarget {\n            get {\n                return ResourceManager.GetString(\"UserIDTarget\", resourceCulture);\n            }\n        }\n        \n        public static string UserImageApprovalNotReq {\n            get {\n                return ResourceManager.GetString(\"UserImageApprovalNotReq\", resourceCulture);\n            }\n        }\n        \n        public static string UseSsl {\n            get {\n                return ResourceManager.GetString(\"UseSsl\", resourceCulture);\n            }\n        }\n        \n        public static string ViewingRoles {\n            get {\n                return ResourceManager.GetString(\"ViewingRoles\", resourceCulture);\n            }\n        }\n        \n        public static string Close {\n            get {\n                return ResourceManager.GetString(\"Close\", resourceCulture);\n            }\n        }\n        \n        public static string CloseOnReply {\n            get {\n                return ResourceManager.GetString(\"CloseOnReply\", resourceCulture);\n            }\n        }\n        \n        public static string FavoriteMake {\n            get {\n                return ResourceManager.GetString(\"FavoriteMake\", resourceCulture);\n            }\n        }\n        \n        public static string FavoriteRemove {\n            get {\n                return ResourceManager.GetString(\"FavoriteRemove\", resourceCulture);\n            }\n        }\n        \n        public static string Link {\n            get {\n                return ResourceManager.GetString(\"Link\", resourceCulture);\n            }\n        }\n        \n        public static string Moderator {\n            get {\n                return ResourceManager.GetString(\"Moderator\", resourceCulture);\n            }\n        }\n        \n        public static string NameAvatar {\n            get {\n                return ResourceManager.GetString(\"NameAvatar\", resourceCulture);\n            }\n        }\n        \n        public static string NameLastEdit {\n            get {\n                return ResourceManager.GetString(\"NameLastEdit\", resourceCulture);\n            }\n        }\n        \n        public static string Open {\n            get {\n                return ResourceManager.GetString(\"Open\", resourceCulture);\n            }\n        }\n        \n        public static string Pin {\n            get {\n                return ResourceManager.GetString(\"Pin\", resourceCulture);\n            }\n        }\n        \n        public static string PostReply {\n            get {\n                return ResourceManager.GetString(\"PostReply\", resourceCulture);\n            }\n        }\n        \n        public static string Quote {\n            get {\n                return ResourceManager.GetString(\"Quote\", resourceCulture);\n            }\n        }\n        \n        public static string RecentTopics {\n            get {\n                return ResourceManager.GetString(\"RecentTopics\", resourceCulture);\n            }\n        }\n        \n        public static string Reply {\n            get {\n                return ResourceManager.GetString(\"Reply\", resourceCulture);\n            }\n        }\n        \n        public static string ShowMorePosts {\n            get {\n                return ResourceManager.GetString(\"ShowMorePosts\", resourceCulture);\n            }\n        }\n        \n        public static string ShowPreviousPosts {\n            get {\n                return ResourceManager.GetString(\"ShowPreviousPosts\", resourceCulture);\n            }\n        }\n        \n        public static string Subscribe {\n            get {\n                return ResourceManager.GetString(\"Subscribe\", resourceCulture);\n            }\n        }\n        \n        public static string Undelete {\n            get {\n                return ResourceManager.GetString(\"Undelete\", resourceCulture);\n            }\n        }\n        \n        public static string Unpin {\n            get {\n                return ResourceManager.GetString(\"Unpin\", resourceCulture);\n            }\n        }\n        \n        public static string Update {\n            get {\n                return ResourceManager.GetString(\"Update\", resourceCulture);\n            }\n        }\n        \n        public static string Archive {\n            get {\n                return ResourceManager.GetString(\"Archive\", resourceCulture);\n            }\n        }\n        \n        public static string NewPM {\n            get {\n                return ResourceManager.GetString(\"NewPM\", resourceCulture);\n            }\n        }\n        \n        public static string NoResults {\n            get {\n                return ResourceManager.GetString(\"NoResults\", resourceCulture);\n            }\n        }\n        \n        public static string Send {\n            get {\n                return ResourceManager.GetString(\"Send\", resourceCulture);\n            }\n        }\n        \n        public static string Unarchive {\n            get {\n                return ResourceManager.GetString(\"Unarchive\", resourceCulture);\n            }\n        }\n        \n        public static string ViewArchivedMessages {\n            get {\n                return ResourceManager.GetString(\"ViewArchivedMessages\", resourceCulture);\n            }\n        }\n        \n        public static string DisplayName {\n            get {\n                return ResourceManager.GetString(\"DisplayName\", resourceCulture);\n            }\n        }\n        \n        public static string Error {\n            get {\n                return ResourceManager.GetString(\"Error\", resourceCulture);\n            }\n        }\n        \n        public static string ErrorSettingUpDb {\n            get {\n                return ResourceManager.GetString(\"ErrorSettingUpDb\", resourceCulture);\n            }\n        }\n        \n        public static string ForumReady {\n            get {\n                return ResourceManager.GetString(\"ForumReady\", resourceCulture);\n            }\n        }\n        \n        public static string ForumTitle {\n            get {\n                return ResourceManager.GetString(\"ForumTitle\", resourceCulture);\n            }\n        }\n        \n        public static string GoAdmin {\n            get {\n                return ResourceManager.GetString(\"GoAdmin\", resourceCulture);\n            }\n        }\n        \n        public static string InvalidLink {\n            get {\n                return ResourceManager.GetString(\"InvalidLink\", resourceCulture);\n            }\n        }\n        \n        public static string NoDataConnection {\n            get {\n                return ResourceManager.GetString(\"NoDataConnection\", resourceCulture);\n            }\n        }\n        \n        public static string PopForumsSetup {\n            get {\n                return ResourceManager.GetString(\"PopForumsSetup\", resourceCulture);\n            }\n        }\n        \n        public static string SetupCantConnect {\n            get {\n                return ResourceManager.GetString(\"SetupCantConnect\", resourceCulture);\n            }\n        }\n        \n        public static string SetupConnSuccess {\n            get {\n                return ResourceManager.GetString(\"SetupConnSuccess\", resourceCulture);\n            }\n        }\n        \n        public static string SetupDatabase {\n            get {\n                return ResourceManager.GetString(\"SetupDatabase\", resourceCulture);\n            }\n        }\n        \n        public static string SetupFirstUser {\n            get {\n                return ResourceManager.GetString(\"SetupFirstUser\", resourceCulture);\n            }\n        }\n        \n        public static string SubscribedTopics {\n            get {\n                return ResourceManager.GetString(\"SubscribedTopics\", resourceCulture);\n            }\n        }\n        \n        public static string SubscribeLoggedIn {\n            get {\n                return ResourceManager.GetString(\"SubscribeLoggedIn\", resourceCulture);\n            }\n        }\n        \n        public static string SubscribeNone {\n            get {\n                return ResourceManager.GetString(\"SubscribeNone\", resourceCulture);\n            }\n        }\n        \n        public static string Success {\n            get {\n                return ResourceManager.GetString(\"Success\", resourceCulture);\n            }\n        }\n        \n        public static string UnsubscribeTopic {\n            get {\n                return ResourceManager.GetString(\"UnsubscribeTopic\", resourceCulture);\n            }\n        }\n        \n        public static string ArchivedNewPosts {\n            get {\n                return ResourceManager.GetString(\"ArchivedNewPosts\", resourceCulture);\n            }\n        }\n        \n        public static string Closed {\n            get {\n                return ResourceManager.GetString(\"Closed\", resourceCulture);\n            }\n        }\n        \n        public static string ClosedPinned {\n            get {\n                return ResourceManager.GetString(\"ClosedPinned\", resourceCulture);\n            }\n        }\n        \n        public static string DeleteTopic {\n            get {\n                return ResourceManager.GetString(\"DeleteTopic\", resourceCulture);\n            }\n        }\n        \n        public static string First {\n            get {\n                return ResourceManager.GetString(\"First\", resourceCulture);\n            }\n        }\n        \n        public static string ForgotPasswordEmail {\n            get {\n                return ResourceManager.GetString(\"ForgotPasswordEmail\", resourceCulture);\n            }\n        }\n        \n        public static string ForgotPasswordSubject {\n            get {\n                return ResourceManager.GetString(\"ForgotPasswordSubject\", resourceCulture);\n            }\n        }\n        \n        public static string ForumNoPost {\n            get {\n                return ResourceManager.GetString(\"ForumNoPost\", resourceCulture);\n            }\n        }\n        \n        public static string ForumNoView {\n            get {\n                return ResourceManager.GetString(\"ForumNoView\", resourceCulture);\n            }\n        }\n        \n        public static string LessThanMinute {\n            get {\n                return ResourceManager.GetString(\"LessThanMinute\", resourceCulture);\n            }\n        }\n        \n        public static string LoginToPost {\n            get {\n                return ResourceManager.GetString(\"LoginToPost\", resourceCulture);\n            }\n        }\n        \n        public static string MinutesAgo {\n            get {\n                return ResourceManager.GetString(\"MinutesAgo\", resourceCulture);\n            }\n        }\n        \n        public static string More {\n            get {\n                return ResourceManager.GetString(\"More\", resourceCulture);\n            }\n        }\n        \n        public static string NewPosts {\n            get {\n                return ResourceManager.GetString(\"NewPosts\", resourceCulture);\n            }\n        }\n        \n        public static string NewPostsClosed {\n            get {\n                return ResourceManager.GetString(\"NewPostsClosed\", resourceCulture);\n            }\n        }\n        \n        public static string NewPostsClosedPinned {\n            get {\n                return ResourceManager.GetString(\"NewPostsClosedPinned\", resourceCulture);\n            }\n        }\n        \n        public static string NewPostsPinned {\n            get {\n                return ResourceManager.GetString(\"NewPostsPinned\", resourceCulture);\n            }\n        }\n        \n        public static string Next {\n            get {\n                return ResourceManager.GetString(\"Next\", resourceCulture);\n            }\n        }\n        \n        public static string NoNewPosts {\n            get {\n                return ResourceManager.GetString(\"NoNewPosts\", resourceCulture);\n            }\n        }\n        \n        public static string NotLoggedIn {\n            get {\n                return ResourceManager.GetString(\"NotLoggedIn\", resourceCulture);\n            }\n        }\n        \n        public static string OneMinuteAgo {\n            get {\n                return ResourceManager.GetString(\"OneMinuteAgo\", resourceCulture);\n            }\n        }\n        \n        public static string Pinned {\n            get {\n                return ResourceManager.GetString(\"Pinned\", resourceCulture);\n            }\n        }\n        \n        public static string PMCreateWarnings {\n            get {\n                return ResourceManager.GetString(\"PMCreateWarnings\", resourceCulture);\n            }\n        }\n        \n        public static string PostEmpty {\n            get {\n                return ResourceManager.GetString(\"PostEmpty\", resourceCulture);\n            }\n        }\n        \n        public static string PostWait {\n            get {\n                return ResourceManager.GetString(\"PostWait\", resourceCulture);\n            }\n        }\n        \n        public static string Previous {\n            get {\n                return ResourceManager.GetString(\"Previous\", resourceCulture);\n            }\n        }\n        \n        public static string RegisterEmailSubject {\n            get {\n                return ResourceManager.GetString(\"RegisterEmailSubject\", resourceCulture);\n            }\n        }\n        \n        public static string RegisterEmailThankYou {\n            get {\n                return ResourceManager.GetString(\"RegisterEmailThankYou\", resourceCulture);\n            }\n        }\n        \n        public static string RegisterEmailThankYouVerify {\n            get {\n                return ResourceManager.GetString(\"RegisterEmailThankYouVerify\", resourceCulture);\n            }\n        }\n        \n        public static string SettingsSaved {\n            get {\n                return ResourceManager.GetString(\"SettingsSaved\", resourceCulture);\n            }\n        }\n        \n        public static string TodayTime {\n            get {\n                return ResourceManager.GetString(\"TodayTime\", resourceCulture);\n            }\n        }\n        \n        public static string TopicNotExist {\n            get {\n                return ResourceManager.GetString(\"TopicNotExist\", resourceCulture);\n            }\n        }\n        \n        public static string YesterdayTime {\n            get {\n                return ResourceManager.GetString(\"YesterdayTime\", resourceCulture);\n            }\n        }\n        \n        public static string Loading {\n            get {\n                return ResourceManager.GetString(\"Loading\", resourceCulture);\n            }\n        }\n        \n        public static string Voted {\n            get {\n                return ResourceManager.GetString(\"Voted\", resourceCulture);\n            }\n        }\n        \n        public static string VoteUpPublishMessage {\n            get {\n                return ResourceManager.GetString(\"VoteUpPublishMessage\", resourceCulture);\n            }\n        }\n        \n        public static string ScoringGame {\n            get {\n                return ResourceManager.GetString(\"ScoringGame\", resourceCulture);\n            }\n        }\n        \n        public static string Awards {\n            get {\n                return ResourceManager.GetString(\"Awards\", resourceCulture);\n            }\n        }\n        \n        public static string EventDefinitions {\n            get {\n                return ResourceManager.GetString(\"EventDefinitions\", resourceCulture);\n            }\n        }\n        \n        public static string AwardDefinitions {\n            get {\n                return ResourceManager.GetString(\"AwardDefinitions\", resourceCulture);\n            }\n        }\n        \n        public static string ManualEvent {\n            get {\n                return ResourceManager.GetString(\"ManualEvent\", resourceCulture);\n            }\n        }\n        \n        public static string None {\n            get {\n                return ResourceManager.GetString(\"None\", resourceCulture);\n            }\n        }\n        \n        public static string ActivityFeed {\n            get {\n                return ResourceManager.GetString(\"ActivityFeed\", resourceCulture);\n            }\n        }\n        \n        public static string NewReplyPublishMessage {\n            get {\n                return ResourceManager.GetString(\"NewReplyPublishMessage\", resourceCulture);\n            }\n        }\n        \n        public static string NewPostPublishMessage {\n            get {\n                return ResourceManager.GetString(\"NewPostPublishMessage\", resourceCulture);\n            }\n        }\n        \n        public static string ExternalLogins {\n            get {\n                return ResourceManager.GetString(\"ExternalLogins\", resourceCulture);\n            }\n        }\n        \n        public static string UseExistingForumAccount {\n            get {\n                return ResourceManager.GetString(\"UseExistingForumAccount\", resourceCulture);\n            }\n        }\n        \n        public static string ExpiredLogin {\n            get {\n                return ResourceManager.GetString(\"ExpiredLogin\", resourceCulture);\n            }\n        }\n        \n        public static string ExternalLoginsDisabled {\n            get {\n                return ResourceManager.GetString(\"ExternalLoginsDisabled\", resourceCulture);\n            }\n        }\n        \n        public static string NoExternalLoginsRegistered {\n            get {\n                return ResourceManager.GetString(\"NoExternalLoginsRegistered\", resourceCulture);\n            }\n        }\n        \n        public static string DeletePermanently {\n            get {\n                return ResourceManager.GetString(\"DeletePermanently\", resourceCulture);\n            }\n        }\n        \n        public static string Preview {\n            get {\n                return ResourceManager.GetString(\"Preview\", resourceCulture);\n            }\n        }\n        \n        public static string AskAQuestion {\n            get {\n                return ResourceManager.GetString(\"AskAQuestion\", resourceCulture);\n            }\n        }\n        \n        public static string Answers {\n            get {\n                return ResourceManager.GetString(\"Answers\", resourceCulture);\n            }\n        }\n        \n        public static string QuestionAnswered {\n            get {\n                return ResourceManager.GetString(\"QuestionAnswered\", resourceCulture);\n            }\n        }\n        \n        public static string ChooseAnswer {\n            get {\n                return ResourceManager.GetString(\"ChooseAnswer\", resourceCulture);\n            }\n        }\n        \n        public static string PostAnswer {\n            get {\n                return ResourceManager.GetString(\"PostAnswer\", resourceCulture);\n            }\n        }\n        \n        public static string SubmitAnswer {\n            get {\n                return ResourceManager.GetString(\"SubmitAnswer\", resourceCulture);\n            }\n        }\n        \n        public static string AppRestartRequired {\n            get {\n                return ResourceManager.GetString(\"AppRestartRequired\", resourceCulture);\n            }\n        }\n        \n        public static string SearchError {\n            get {\n                return ResourceManager.GetString(\"SearchError\", resourceCulture);\n            }\n        }\n        \n        public static string BotError {\n            get {\n                return ResourceManager.GetString(\"BotError\", resourceCulture);\n            }\n        }\n        \n        public static string CloseOldTopics {\n            get {\n                return ResourceManager.GetString(\"CloseOldTopics\", resourceCulture);\n            }\n        }\n        \n        public static string Days {\n            get {\n                return ResourceManager.GetString(\"Days\", resourceCulture);\n            }\n        }\n        \n        public static string PrivateForum {\n            get {\n                return ResourceManager.GetString(\"PrivateForum\", resourceCulture);\n            }\n        }\n        \n        public static string AccountNotVerified {\n            get {\n                return ResourceManager.GetString(\"AccountNotVerified\", resourceCulture);\n            }\n        }\n        \n        public static string SelectText {\n            get {\n                return ResourceManager.GetString(\"SelectText\", resourceCulture);\n            }\n        }\n        \n        public static string Notifications {\n            get {\n                return ResourceManager.GetString(\"Notifications\", resourceCulture);\n            }\n        }\n        \n        public static string NewReplyNotification {\n            get {\n                return ResourceManager.GetString(\"NewReplyNotification\", resourceCulture);\n            }\n        }\n        \n        public static string Award {\n            get {\n                return ResourceManager.GetString(\"Award\", resourceCulture);\n            }\n        }\n        \n        public static string VoteUpNotification {\n            get {\n                return ResourceManager.GetString(\"VoteUpNotification\", resourceCulture);\n            }\n        }\n        \n        public static string QuestionAnsweredNotification {\n            get {\n                return ResourceManager.GetString(\"QuestionAnsweredNotification\", resourceCulture);\n            }\n        }\n        \n        public static string AutoFollow {\n            get {\n                return ResourceManager.GetString(\"AutoFollow\", resourceCulture);\n            }\n        }\n        \n        public static string MarkAllRead {\n            get {\n                return ResourceManager.GetString(\"MarkAllRead\", resourceCulture);\n            }\n        }\n        \n        public static string UploadImage {\n            get {\n                return ResourceManager.GetString(\"UploadImage\", resourceCulture);\n            }\n        }\n        \n        public static string Ignored {\n            get {\n                return ResourceManager.GetString(\"Ignored\", resourceCulture);\n            }\n        }\n        \n        public static string Ignore {\n            get {\n                return ResourceManager.GetString(\"Ignore\", resourceCulture);\n            }\n        }\n        \n        public static string IgnoreList {\n            get {\n                return ResourceManager.GetString(\"IgnoreList\", resourceCulture);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/PopForums/Resources/Resources.de.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"Account\" xml:space=\"preserve\">\n    <value>Account</value>\n  </data>\n  <data name=\"AccountCreated\" xml:space=\"preserve\">\n    <value>Ihr Account wurde erstellt.</value>\n  </data>\n  <data name=\"AccountReady\" xml:space=\"preserve\">\n    <value>Ihr Account wurde erstellt und Sie können nun das Forum nutzen.</value>\n  </data>\n  <data name=\"AccountReadyCheckEmail\" xml:space=\"preserve\">\n    <value>Um das Forum zu nutzen bestätigen Sie bitte den Link in der e-Mail (wurde an Sie versendet).</value>\n  </data>\n  <data name=\"AccountVerified\" xml:space=\"preserve\">\n    <value>Ihr Account wurde überprüft.</value>\n  </data>\n  <data name=\"AlreadyCreatedAccount\" xml:space=\"preserve\">\n    <value>Sie sind eingeloggt.</value>\n  </data>\n  <data name=\"AsYouWouldLikeItToAppear\" xml:space=\"preserve\">\n    <value>Erscheint, freigewählt, so wie Sie es möchten.</value>\n  </data>\n  <data name=\"AtLeastSixChar\" xml:space=\"preserve\">\n    <value>mindestens 6 Zeichen</value>\n  </data>\n  <data name=\"By\" xml:space=\"preserve\">\n    <value>by</value>\n    <comment>Kontext: \"Letzte Nachricht:12 Uhr by Jeff</comment>\n  </data>\n  <data name=\"CreateAccountButton\" xml:space=\"preserve\">\n    <value>Account erstellen</value>\n  </data>\n  <data name=\"CreateAnAccount\" xml:space=\"preserve\">\n    <value>Account erstellen</value>\n  </data>\n  <data name=\"EditYourProfile\" xml:space=\"preserve\">\n    <value>Profil editieren</value>\n  </data>\n  <data name=\"Email\" xml:space=\"preserve\">\n    <value>E-mail</value>\n  </data>\n  <data name=\"EmailProblemAccount\" xml:space=\"preserve\">\n    <value>Die Bestätigungs e-mail konnte nicht gesendet werden:</value>\n  </data>\n  <data name=\"Favorites\" xml:space=\"preserve\">\n    <value>Favoriten</value>\n  </data>\n  <data name=\"Forbidden\" xml:space=\"preserve\">\n    <value>Sorry, Sie haben keine Berechtigung für diese Ansicht.</value>\n  </data>\n  <data name=\"Forums\" xml:space=\"preserve\">\n    <value>Forum</value>\n  </data>\n  <data name=\"HaveReadTOS\" xml:space=\"preserve\">\n    <value>Ich habe die Nutzungbedingungen gelesen und akzeptiert.</value>\n  </data>\n  <data name=\"Last\" xml:space=\"preserve\">\n    <value>Letzte</value>\n  </data>\n  <data name=\"Login\" xml:space=\"preserve\">\n    <value>Login</value>\n  </data>\n  <data name=\"Logout\" xml:space=\"preserve\">\n    <value>Logout</value>\n  </data>\n  <data name=\"MarkAllForumsRead\" xml:space=\"preserve\">\n    <value>Alle gelesenen Foren markieren</value>\n  </data>\n  <data name=\"MyBirthdayIsOnOrBefore\" xml:space=\"preserve\">\n    <value>Mein Geburtstag ist am oder vor {0}</value>\n    <comment>Wo {0} ist ein Datum</comment>\n  </data>\n  <data name=\"Name\" xml:space=\"preserve\">\n    <value>Name</value>\n  </data>\n  <data name=\"NeedToVerifyExistingAccount\" xml:space=\"preserve\">\n    <value>Möchten Sie ein bestehendes account bestätigen?</value>\n  </data>\n  <data name=\"PageNotFound\" xml:space=\"preserve\">\n    <value>Seite nicht gefunden</value>\n  </data>\n  <data name=\"Password\" xml:space=\"preserve\">\n    <value>Passwort</value>\n  </data>\n  <data name=\"Posts\" xml:space=\"preserve\">\n    <value>Einträge</value>\n  </data>\n  <data name=\"PrivateMessages\" xml:space=\"preserve\">\n    <value>Private Nachrichten</value>\n  </data>\n  <data name=\"Recent\" xml:space=\"preserve\">\n    <value>Neueste</value>\n  </data>\n  <data name=\"RegisteredUsers\" xml:space=\"preserve\">\n    <value>Registrierte Mitglieder</value>\n  </data>\n  <data name=\"RetypePassword\" xml:space=\"preserve\">\n    <value>Passwort wiederholen</value>\n  </data>\n  <data name=\"Search\" xml:space=\"preserve\">\n    <value>Suche</value>\n  </data>\n  <data name=\"SubscribeToList\" xml:space=\"preserve\">\n    <value>Eintragen (Abbonieren) in die Mailing-Liste</value>\n  </data>\n  <data name=\"Subscriptions\" xml:space=\"preserve\">\n    <value>Abbonements</value>\n  </data>\n  <data name=\"TermsOfService\" xml:space=\"preserve\">\n    <value>Nutzungsbedingungen</value>\n  </data>\n  <data name=\"TimeZone\" xml:space=\"preserve\">\n    <value>Zeitzone</value>\n  </data>\n  <data name=\"Topics\" xml:space=\"preserve\">\n    <value>Themen</value>\n  </data>\n  <data name=\"Total\" xml:space=\"preserve\">\n    <value>Gesamt</value>\n  </data>\n  <data name=\"TotalPosts\" xml:space=\"preserve\">\n    <value>Gesamte Einträge</value>\n  </data>\n  <data name=\"TotalTopics\" xml:space=\"preserve\">\n    <value>Alle Themen</value>\n  </data>\n  <data name=\"UseDaylight\" xml:space=\"preserve\">\n    <value>EU-Zeitverschiebung nutzen</value>\n  </data>\n  <data name=\"UsersOnline\" xml:space=\"preserve\">\n    <value>User Online</value>\n  </data>\n  <data name=\"MustBe13\" xml:space=\"preserve\">\n    <value>Für eine Registrierung müssen Sie 13 Jahre oder älter sein</value>\n  </data>\n  <data name=\"MustAcceptTOS\" xml:space=\"preserve\">\n    <value>Sie müssen die Teilnahmebedingungen akzeptieren</value>\n  </data>\n  <data name=\"RetypeYourPassword\" xml:space=\"preserve\">\n    <value>Geben Bitte Sie Ihr Passwort erneut ein</value>\n  </data>\n  <data name=\"NameRequired\" xml:space=\"preserve\">\n    <value>Name erforderlich</value>\n  </data>\n  <data name=\"NameInUse\" xml:space=\"preserve\">\n    <value>Dieser Name ist schon vergeben</value>\n  </data>\n  <data name=\"EmailRequired\" xml:space=\"preserve\">\n    <value>E-mail erforderlich</value>\n  </data>\n  <data name=\"ValidEmailAddressRequired\" xml:space=\"preserve\">\n    <value>Gültige e-Mail Adresse erforderlich</value>\n  </data>\n  <data name=\"EmailInUse\" xml:space=\"preserve\">\n    <value>Diese e-Mail Adresse ist schon vergeben.</value>\n  </data>\n  <data name=\"EmailBanned\" xml:space=\"preserve\">\n    <value>Diese e-Mail Adresse wurde gesperrt.</value>\n  </data>\n  <data name=\"IPBanned\" xml:space=\"preserve\">\n    <value>Ihre IP-Adresse wurde gesperrt</value>\n  </data>\n  <data name=\"RetypePasswordMustMatch\" xml:space=\"preserve\">\n    <value>Das Passwort stimmt nicht überein.</value>\n  </data>\n  <data name=\"EditAccount\" xml:space=\"preserve\">\n    <value>Account bearbeiten</value>\n  </data>\n  <data name=\"EditProfile\" xml:space=\"preserve\">\n    <value>Profil bearbeiten</value>\n  </data>\n  <data name=\"EnterVerificationCode\" xml:space=\"preserve\">\n    <value>Geben Sie bitte Ihren Überprüfungs-Code ein.</value>\n  </data>\n  <data name=\"ForumHomePage\" xml:space=\"preserve\">\n    <value>Startseite</value>\n  </data>\n  <data name=\"MustBeRegisteredToEditAccount\" xml:space=\"preserve\">\n    <value>Sie müssen registriert und eingeloggt sein um Ihren Account bearbeiten zu können.</value>\n  </data>\n  <data name=\"NoUserFoundWithEmail\" xml:space=\"preserve\">\n    <value>Kein Teilnehmer mit dieser e-Mail gefunden.</value>\n  </data>\n  <data name=\"SendEmailWithNewCodeButton\" xml:space=\"preserve\">\n    <value>Neue e-Mail mit Code senden</value>\n  </data>\n  <data name=\"VerificationEmailSent\" xml:space=\"preserve\">\n    <value>Ihre bestätigte e-Mail wurde versendet.</value>\n  </data>\n  <data name=\"VerificationFailure\" xml:space=\"preserve\">\n    <value>Bestätigung fehlerhaft</value>\n  </data>\n  <data name=\"VerificationIfYouNeed\" xml:space=\"preserve\">\n    <value>Wenn Sie einen neuen Bestätigungs-Code benötigen, tragen Sie Ihre e-Mail hier ein.</value>\n  </data>\n  <data name=\"VerificationLinkBad\" xml:space=\"preserve\">\n    <value>Sorry, der Bestätigungs-Link oder eingegebene Code ist fehlerhaft.</value>\n  </data>\n  <data name=\"VerifyAccount\" xml:space=\"preserve\">\n    <value>Überprüfen Sie Ihr Konto.</value>\n  </data>\n  <data name=\"VerifyCodeButton\" xml:space=\"preserve\">\n    <value>Bestätigungs-Code</value>\n  </data>\n  <data name=\"Avatar\" xml:space=\"preserve\">\n    <value>Avatar</value>\n  </data>\n  <data name=\"AvatarDelete\" xml:space=\"preserve\">\n    <value>Avatar löschen</value>\n  </data>\n  <data name=\"ChangeEmail\" xml:space=\"preserve\">\n    <value>e-Mail erneuern</value>\n  </data>\n  <data name=\"ChangeEmailButton\" xml:space=\"preserve\">\n    <value>e-Mail erneuern und Code senden</value>\n  </data>\n  <data name=\"ChangeEmailConsequence\" xml:space=\"preserve\">\n    <value>Das wechseln der e-Mail Adresse wird einen neuen Bestätigungs-Code auslösen der an die neue e-Mail Adresse gesendet wird. Bevor Sie neue Einträge im Forum vornehmen können, müssen Sie den neuen Code mit der neuen e-Mail Adresse bestätigen.</value>\n  </data>\n  <data name=\"ChangePassword\" xml:space=\"preserve\">\n    <value>Passwort ändern</value>\n  </data>\n  <data name=\"ChangeYourEmailPassword\" xml:space=\"preserve\">\n    <value>Ändern Sie bitte Ihre e-Mail oder Ihr Passwort</value>\n  </data>\n  <data name=\"DateOfBirth\" xml:space=\"preserve\">\n    <value>Geburtsdatum</value>\n  </data>\n  <data name=\"Details\" xml:space=\"preserve\">\n    <value>Details</value>\n  </data>\n  <data name=\"EmailChangeSuccess\" xml:space=\"preserve\">\n    <value>Ihre e-Mail wurde gewechselt</value>\n  </data>\n  <data name=\"EmailNew\" xml:space=\"preserve\">\n    <value>Neue e-Mail Adresse</value>\n  </data>\n  <data name=\"EmailNewRetype\" xml:space=\"preserve\">\n    <value>Neue e-Mail Adresse wiederholen</value>\n  </data>\n  <data name=\"EmailNotFound\" xml:space=\"preserve\">\n    <value>e-Mail nicht gefunden</value>\n  </data>\n  <data name=\"EmailSent\" xml:space=\"preserve\">\n    <value>Ihre e-Mail wurde gesendet</value>\n  </data>\n  <data name=\"EmailsMustMatch\" xml:space=\"preserve\">\n    <value>Diese e-Mail Adresse stimmt nicht überein</value>\n  </data>\n  <data name=\"EmailUser\" xml:space=\"preserve\">\n    <value>e-Mail User</value>\n  </data>\n  <data name=\"ForcePlainTextBox\" xml:space=\"preserve\">\n    <value>Normal-Text</value>\n  </data>\n  <data name=\"ForgotInstructions\" xml:space=\"preserve\">\n    <value>Geben Sie die e-Mail Adresse ein die im Account hinterlegt ist, um eine Anleitung zur Erneuerung ihres Passwortes zu erhalten.</value>\n  </data>\n  <data name=\"ForgotInstructionsSent\" xml:space=\"preserve\">\n    <value>Die Anleitung wurde zu Ihrer e-Mail Adresse gesendet</value>\n  </data>\n  <data name=\"ForgotPassword\" xml:space=\"preserve\">\n    <value>Passwort vergessen</value>\n  </data>\n  <data name=\"ForgotPasswordQuestion\" xml:space=\"preserve\">\n    <value>Passwort vergessen?</value>\n  </data>\n  <data name=\"HideVanity\" xml:space=\"preserve\">\n    <value>Hide vanity</value>\n  </data>\n  <data name=\"In\" xml:space=\"preserve\">\n    <value>in</value>\n    <comment>Kontext: gestarted von Jeff im Hauptforum</comment>\n  </data>\n  <data name=\"Joined\" xml:space=\"preserve\">\n    <value>Verbinden</value>\n  </data>\n  <data name=\"Location\" xml:space=\"preserve\">\n    <value>Ort</value>\n  </data>\n  <data name=\"LoginAlready\" xml:space=\"preserve\">\n    <value>Sie sind eingeloggt.</value>\n  </data>\n  <data name=\"LoginBad\" xml:space=\"preserve\">\n    <value>Falsche e-Mail oder Passwort</value>\n  </data>\n  <data name=\"ManagePhotos\" xml:space=\"preserve\">\n    <value>Foto-Verwaltung</value>\n  </data>\n  <data name=\"ManageYourPhotos\" xml:space=\"preserve\">\n    <value>Fotos verwalten</value>\n  </data>\n  <data name=\"NewPasswordSaved\" xml:space=\"preserve\">\n    <value>Neues Passwort gespeichert</value>\n  </data>\n  <data name=\"NotRegisteredQuestion\" xml:space=\"preserve\">\n    <value>Nicht registriert?</value>\n  </data>\n  <data name=\"OldPasswordIncorrect\" xml:space=\"preserve\">\n    <value>Altes Passwort nicht korrekt</value>\n  </data>\n  <data name=\"Options\" xml:space=\"preserve\">\n    <value>Optionen</value>\n  </data>\n  <data name=\"PasswordNew\" xml:space=\"preserve\">\n    <value>Neues Passwort</value>\n  </data>\n  <data name=\"PasswordNewRetype\" xml:space=\"preserve\">\n    <value>Neues Passwort wiederholen</value>\n  </data>\n  <data name=\"PasswordOld\" xml:space=\"preserve\">\n    <value>Altes Passwort</value>\n  </data>\n  <data name=\"PasswordReset\" xml:space=\"preserve\">\n    <value>Passwort erneuern</value>\n  </data>\n  <data name=\"PasswordResetLinkInvalid\" xml:space=\"preserve\">\n    <value>Dies ist kein gültiger Passwort-Erneuerungs-Link.</value>\n  </data>\n  <data name=\"PasswordResetNote\" xml:space=\"preserve\">\n    <value>Ihr neues Passwort ist aktiv und Sie sind eingeloggt.</value>\n  </data>\n  <data name=\"PasswordResetSuccess\" xml:space=\"preserve\">\n    <value>Die Passwort Erneuerung war erfolgreich</value>\n  </data>\n  <data name=\"Photo\" xml:space=\"preserve\">\n    <value>Foto</value>\n  </data>\n  <data name=\"PhotoDelete\" xml:space=\"preserve\">\n    <value>Foto löschen</value>\n  </data>\n  <data name=\"PhotoNotApproved\" xml:space=\"preserve\">\n    <value>(dieses Bild wurde noch nicht durch den Administrator freigegeben)</value>\n  </data>\n  <data name=\"ProfileUpdated\" xml:space=\"preserve\">\n    <value>Profil updaten</value>\n  </data>\n  <data name=\"RememberMe\" xml:space=\"preserve\">\n    <value>Erinnnern Sie mich?</value>\n  </data>\n  <data name=\"Replies\" xml:space=\"preserve\">\n    <value>Antworten</value>\n  </data>\n  <data name=\"Save\" xml:space=\"preserve\">\n    <value>Speichern</value>\n  </data>\n  <data name=\"SendEmailButton\" xml:space=\"preserve\">\n    <value>e-Mail senden</value>\n  </data>\n  <data name=\"SendPM\" xml:space=\"preserve\">\n    <value>Private Nachricht senden</value>\n  </data>\n  <data name=\"ShowProfileDetails\" xml:space=\"preserve\">\n    <value>Zeigt dein Profil (aber nicht die e-Mail)</value>\n  </data>\n  <data name=\"Signature\" xml:space=\"preserve\">\n    <value>Signatur</value>\n  </data>\n  <data name=\"StartedBy\" xml:space=\"preserve\">\n    <value>Gestartet von</value>\n  </data>\n  <data name=\"Subject\" xml:space=\"preserve\">\n    <value>Thema</value>\n  </data>\n  <data name=\"Text\" xml:space=\"preserve\">\n    <value>Text</value>\n  </data>\n  <data name=\"To\" xml:space=\"preserve\">\n    <value>nach</value>\n  </data>\n  <data name=\"UploadNew\" xml:space=\"preserve\">\n    <value>neu Upload</value>\n  </data>\n  <data name=\"UserNotFound\" xml:space=\"preserve\">\n    <value>Teilnehmer nicht gefunden</value>\n  </data>\n  <data name=\"Views\" xml:space=\"preserve\">\n    <value>Views</value>\n  </data>\n  <data name=\"Web\" xml:space=\"preserve\">\n    <value>Web</value>\n  </data>\n  <data name=\"WebVisit\" xml:space=\"preserve\">\n    <value>Besuchen Sie unsere Website</value>\n  </data>\n  <data name=\"YourIP\" xml:space=\"preserve\">\n    <value>Ihre IP-Adresse</value>\n  </data>\n  <data name=\"Birthday\" xml:space=\"preserve\">\n    <value>Geburtstag</value>\n  </data>\n  <data name=\"Contact\" xml:space=\"preserve\">\n    <value>Kontakt</value>\n  </data>\n  <data name=\"Profile\" xml:space=\"preserve\">\n    <value>Profil</value>\n  </data>\n  <data name=\"SendNameEmail\" xml:space=\"preserve\">\n    <value>{0} hat eine Nachricht gesendet</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"SendNamePM\" xml:space=\"preserve\">\n    <value>Send {0} eine private Nachricht</value>\n    <comment>{0} ist ein Name</comment>\n  </data>\n  <data name=\"Unsubscribe\" xml:space=\"preserve\">\n    <value>Abbestellen</value>\n  </data>\n  <data name=\"UnsubscribeFail\" xml:space=\"preserve\">\n    <value>Abbestellung fehlerhaft</value>\n  </data>\n  <data name=\"UnsubscribeLinkBad\" xml:space=\"preserve\">\n    <value>Der Link ist nicht gültig.</value>\n  </data>\n  <data name=\"UnsubscribeNote\" xml:space=\"preserve\">\n    <value>Ihre Abbestellung (Austragung) war erfolgreich. Sie können sich nun in Ihren Account neu einloggen.</value>\n  </data>\n  <data name=\"AddForum\" xml:space=\"preserve\">\n    <value>neues Forum</value>\n  </data>\n  <data name=\"AddNew\" xml:space=\"preserve\">\n    <value>Neu</value>\n  </data>\n  <data name=\"AllowImages\" xml:space=\"preserve\">\n    <value>Bilder erlaubt</value>\n  </data>\n  <data name=\"Archived\" xml:space=\"preserve\">\n    <value>Archiviert</value>\n  </data>\n  <data name=\"AreYouSure\" xml:space=\"preserve\">\n    <value>Sind Sie sicher?</value>\n  </data>\n  <data name=\"Ban\" xml:space=\"preserve\">\n    <value>Sperren</value>\n  </data>\n  <data name=\"BanRemove\" xml:space=\"preserve\">\n    <value>Sperrung verschieben</value>\n  </data>\n  <data name=\"Categories\" xml:space=\"preserve\">\n    <value>Kategorien</value>\n  </data>\n  <data name=\"Category\" xml:space=\"preserve\">\n    <value>Kategorie</value>\n  </data>\n  <data name=\"CategoryEditTitle\" xml:space=\"preserve\">\n    <value>Titel der Kategorie ändern</value>\n  </data>\n  <data name=\"CategoryMoveError\" xml:space=\"preserve\">\n    <value>Es gab einen unbekannten Fehler, beim Versuch die Kategorie zu verschieben.</value>\n  </data>\n  <data name=\"CensorReplacementChar\" xml:space=\"preserve\">\n    <value>ersetzte zensierte Buchstaben</value>\n  </data>\n  <data name=\"CensorWords\" xml:space=\"preserve\">\n    <value>Zensierte Wörter</value>\n  </data>\n  <data name=\"Delete\" xml:space=\"preserve\">\n    <value>Löschen</value>\n  </data>\n  <data name=\"DeleteAndBanButton\" xml:space=\"preserve\">\n    <value>Lösche und sperre Teilnehmer</value>\n  </data>\n  <data name=\"DeleteUserButton\" xml:space=\"preserve\">\n    <value>Lösche Teilnehmer</value>\n  </data>\n  <data name=\"Description\" xml:space=\"preserve\">\n    <value>Beschreibung</value>\n  </data>\n  <data name=\"Down\" xml:space=\"preserve\">\n    <value>Abwärts</value>\n  </data>\n  <data name=\"Edit\" xml:space=\"preserve\">\n    <value>Bearbeiten</value>\n  </data>\n  <data name=\"EditForum\" xml:space=\"preserve\">\n    <value>Forum bearbeiten</value>\n  </data>\n  <data name=\"EditUser\" xml:space=\"preserve\">\n    <value>Teilnehmer bearbeiten</value>\n  </data>\n  <data name=\"EmailBan\" xml:space=\"preserve\">\n    <value>e-Mail sperren</value>\n  </data>\n  <data name=\"EmailIpBan\" xml:space=\"preserve\">\n    <value>e-Mail/IP sperren</value>\n  </data>\n  <data name=\"EmailNewOptional\" xml:space=\"preserve\">\n    <value>Neue e-Mail (optional)</value>\n  </data>\n  <data name=\"EmailUsers\" xml:space=\"preserve\">\n    <value>e-Mail Teilnehmer</value>\n  </data>\n  <data name=\"ErrorLog\" xml:space=\"preserve\">\n    <value>ErrorLog</value>\n  </data>\n  <data name=\"Forum\" xml:space=\"preserve\">\n    <value>Forum</value>\n  </data>\n  <data name=\"ForumAdapter\" xml:space=\"preserve\">\n    <value>Forum Adapter (fakultativ, verwenden \"Namespace.Type, AssemblyName\")</value>\n  </data>\n  <data name=\"ForumHome\" xml:space=\"preserve\">\n    <value>Forum Home</value>\n  </data>\n  <data name=\"ForumPermisions\" xml:space=\"preserve\">\n    <value>Forum Erlaubnis</value>\n  </data>\n  <data name=\"ForumSettings\" xml:space=\"preserve\">\n    <value>Forum Einstellungen</value>\n  </data>\n  <data name=\"ForumsUncat\" xml:space=\"preserve\">\n    <value>Unkategorisiertes Foren</value>\n  </data>\n  <data name=\"GeneralSettings\" xml:space=\"preserve\">\n    <value>Haupteinstellungen</value>\n  </data>\n  <data name=\"IpBan\" xml:space=\"preserve\">\n    <value>IP gesperrt</value>\n  </data>\n  <data name=\"IpHistory\" xml:space=\"preserve\">\n    <value>IP History</value>\n  </data>\n  <data name=\"IsApproved\" xml:space=\"preserve\">\n    <value>Ist geprüft</value>\n  </data>\n  <data name=\"IsSubscribed\" xml:space=\"preserve\">\n    <value>Ist eingetragen (abboniert)</value>\n  </data>\n  <data name=\"LogErrors\" xml:space=\"preserve\">\n    <value>Log Fehler</value>\n  </data>\n  <data name=\"Logging\" xml:space=\"preserve\">\n    <value>Logging</value>\n  </data>\n  <data name=\"LogMod\" xml:space=\"preserve\">\n    <value>Log moderation</value>\n  </data>\n  <data name=\"LogSecurity\" xml:space=\"preserve\">\n    <value>Log Sicherheits Aktionen</value>\n  </data>\n  <data name=\"MinimumTimeBetweenPosts\" xml:space=\"preserve\">\n    <value>Minimum (Sekunden) zwischen den Posts</value>\n  </data>\n  <data name=\"ModerationLog\" xml:space=\"preserve\">\n    <value>Moderation Log</value>\n  </data>\n  <data name=\"Move\" xml:space=\"preserve\">\n    <value>Verschieben</value>\n  </data>\n  <data name=\"NewUserApprovedWithoutVerification\" xml:space=\"preserve\">\n    <value>Neues Mitglied wurde genehmigt ohne Überprüfung</value>\n  </data>\n  <data name=\"NewUserImageApprovedWithoutMod\" xml:space=\"preserve\">\n    <value>Neues Mitglieder-Bild wurde genehmigt ohne Moderation</value>\n  </data>\n  <data name=\"Parsing\" xml:space=\"preserve\">\n    <value>Saztgliederung</value>\n  </data>\n  <data name=\"PasswordNewOptional\" xml:space=\"preserve\">\n    <value>Neues Passwort (optional)</value>\n  </data>\n  <data name=\"PopForumsAdmin\" xml:space=\"preserve\">\n    <value>POP Forums Administration</value>\n  </data>\n  <data name=\"PostsPerPage\" xml:space=\"preserve\">\n    <value>Posts pro Seite</value>\n  </data>\n  <data name=\"Role\" xml:space=\"preserve\">\n    <value>Rolle</value>\n  </data>\n  <data name=\"Roles\" xml:space=\"preserve\">\n    <value>Rollen</value>\n  </data>\n  <data name=\"Security\" xml:space=\"preserve\">\n    <value>Sicherheit</value>\n  </data>\n  <data name=\"SecurityLog\" xml:space=\"preserve\">\n    <value>Security Log</value>\n  </data>\n  <data name=\"ServerTime\" xml:space=\"preserve\">\n    <value>Server Zeit</value>\n  </data>\n  <data name=\"Services\" xml:space=\"preserve\">\n    <value>Services</value>\n  </data>\n  <data name=\"SessionLength\" xml:space=\"preserve\">\n    <value>Session Zeit (Minuten)</value>\n  </data>\n  <data name=\"Size\" xml:space=\"preserve\">\n    <value>Size</value>\n  </data>\n  <data name=\"SubjectAndBodyNotEmpty\" xml:space=\"preserve\">\n    <value>Thema und Beschreibung dürfen nicht leer sein.</value>\n  </data>\n  <data name=\"Title\" xml:space=\"preserve\">\n    <value>Titel</value>\n  </data>\n  <data name=\"TopicsPerPage\" xml:space=\"preserve\">\n    <value>Themen pro Seite</value>\n  </data>\n  <data name=\"Up\" xml:space=\"preserve\">\n    <value>Up</value>\n  </data>\n  <data name=\"UserAvatarMaxDim\" xml:space=\"preserve\">\n    <value>Maximal Grösse des Mitglied-Avatars</value>\n  </data>\n  <data name=\"UserDeleteWarning\" xml:space=\"preserve\">\n    <value>WARNUNG! Das Löschen des Accounts ist unwiederruflich</value>\n  </data>\n  <data name=\"UserImageApproval\" xml:space=\"preserve\">\n    <value>Bild des Mitglieds geprüft</value>\n  </data>\n  <data name=\"UserImageMaxDim\" xml:space=\"preserve\">\n    <value>Maximale Bild-Größe des Mitglieds</value>\n  </data>\n  <data name=\"UserRoles\" xml:space=\"preserve\">\n    <value>Mitglieder Rollen</value>\n  </data>\n  <data name=\"Visible\" xml:space=\"preserve\">\n    <value>Sichtbar</value>\n  </data>\n  <data name=\"AddNewForum\" xml:space=\"preserve\">\n    <value>Neues Forum hinzufügen</value>\n  </data>\n  <data name=\"Body\" xml:space=\"preserve\">\n    <value>Body</value>\n  </data>\n  <data name=\"BodyHtml\" xml:space=\"preserve\">\n    <value>HTML Body</value>\n  </data>\n  <data name=\"ClickToLoadMorePosts\" xml:space=\"preserve\">\n    <value>Hier klicken um neuere Posts vor dem Antworten zu laden.</value>\n  </data>\n  <data name=\"Comment\" xml:space=\"preserve\">\n    <value>Kommentar</value>\n  </data>\n  <data name=\"CommentsOptional\" xml:space=\"preserve\">\n    <value>Kommentar (optional)</value>\n  </data>\n  <data name=\"CreateJunkWordButton\" xml:space=\"preserve\">\n    <value>Neues Junk-Wort erstellen</value>\n  </data>\n  <data name=\"CreateNewReply\" xml:space=\"preserve\">\n    <value>Antwort-Erstellen</value>\n  </data>\n  <data name=\"CreateNewRole\" xml:space=\"preserve\">\n    <value>Neue Rolle erstellen</value>\n  </data>\n  <data name=\"CreateNewTopic\" xml:space=\"preserve\">\n    <value>Neues Thema erstellen</value>\n  </data>\n  <data name=\"DeleteAllErrors\" xml:space=\"preserve\">\n    <value>Alle Fehler löschen</value>\n  </data>\n  <data name=\"DeleteJunkWordButton\" xml:space=\"preserve\">\n    <value>Ausgewähltes Junk-Wort löschen</value>\n  </data>\n  <data name=\"DeleteSelectedRole\" xml:space=\"preserve\">\n    <value>Ausgewählte Rolle löschen</value>\n  </data>\n  <data name=\"EditPost\" xml:space=\"preserve\">\n    <value>Eintrag ändern</value>\n  </data>\n  <data name=\"EmailUsersQueued\" xml:space=\"preserve\">\n    <value>e-Mail in der Warteschlange zu abbonierten (eingetragenen) Mitgliedern.</value>\n  </data>\n  <data name=\"EndDate\" xml:space=\"preserve\">\n    <value>Ende des Datums</value>\n  </data>\n  <data name=\"Event\" xml:space=\"preserve\">\n    <value>Event</value>\n  </data>\n  <data name=\"EventTime\" xml:space=\"preserve\">\n    <value>Event-Zeit</value>\n  </data>\n  <data name=\"FavoriteMustBeLoggedIn\" xml:space=\"preserve\">\n    <value>Sie müssen eingeloggt sein um die Favoriten-Themen zu sehen.</value>\n  </data>\n  <data name=\"FavoritesDontHave\" xml:space=\"preserve\">\n    <value>Sie haben keine Favoriten-Themen.</value>\n  </data>\n  <data name=\"FavoriteTopics\" xml:space=\"preserve\">\n    <value>Favoriten Themen</value>\n  </data>\n  <data name=\"ForumMoveError\" xml:space=\"preserve\">\n    <value>Es gab einen unbekannten Fehler, beim Versuch das Forum zu verschieben.</value>\n  </data>\n  <data name=\"ForumPermissionInstructions\" xml:space=\"preserve\">\n    <value>Um den Eintrag zu verschieben oder die Restriktionen anzuzeigen, verschiebe alle Rollen von der passenden Box.</value>\n  </data>\n  <data name=\"FromEmailAddress\" xml:space=\"preserve\">\n    <value>Von der e-Mail Adresse</value>\n  </data>\n  <data name=\"ID\" xml:space=\"preserve\">\n    <value>ID</value>\n  </data>\n  <data name=\"IncludeSignature\" xml:space=\"preserve\">\n    <value>mit Signatur</value>\n  </data>\n  <data name=\"IndexingInterval\" xml:space=\"preserve\">\n    <value>Indizierter Intervall</value>\n  </data>\n  <data name=\"IP\" xml:space=\"preserve\">\n    <value>IP</value>\n  </data>\n  <data name=\"IsRunning\" xml:space=\"preserve\">\n    <value>Ist am Laufen</value>\n  </data>\n  <data name=\"JunkWords\" xml:space=\"preserve\">\n    <value>Junk-Wort</value>\n  </data>\n  <data name=\"MarkForumRead\" xml:space=\"preserve\">\n    <value>markiere Forum gelesen</value>\n  </data>\n  <data name=\"Message\" xml:space=\"preserve\">\n    <value>Nachricht</value>\n  </data>\n  <data name=\"Millseconds\" xml:space=\"preserve\">\n    <value>Millisekunden</value>\n  </data>\n  <data name=\"NamePosts\" xml:space=\"preserve\">\n    <value>{0}s posts</value>\n    <comment>{0} ist ein Name</comment>\n  </data>\n  <data name=\"Optional\" xml:space=\"preserve\">\n    <value>(fakultativ)</value>\n  </data>\n  <data name=\"PermRoles\" xml:space=\"preserve\">\n    <value>Administrator und Moderator sind permanente Rollen, und kann nicht gelöscht werden.</value>\n  </data>\n  <data name=\"PostID\" xml:space=\"preserve\">\n    <value>PostID</value>\n  </data>\n  <data name=\"PostingRoles\" xml:space=\"preserve\">\n    <value>gepostete Rollen</value>\n  </data>\n  <data name=\"PostNewTopic\" xml:space=\"preserve\">\n    <value>Neue Rolle posten</value>\n  </data>\n  <data name=\"PreviewTopic\" xml:space=\"preserve\">\n    <value>Thema-Vorschau</value>\n    <comment>Verb-Vorschau, nicht Adjektive</comment>\n  </data>\n  <data name=\"Remove\" xml:space=\"preserve\">\n    <value>Verschieben</value>\n  </data>\n  <data name=\"RemoveAll\" xml:space=\"preserve\">\n    <value>Alles verschieben</value>\n  </data>\n  <data name=\"SendingInterval\" xml:space=\"preserve\">\n    <value>Interval senden</value>\n  </data>\n  <data name=\"SmtpPassword\" xml:space=\"preserve\">\n    <value>SMTP Passwort</value>\n  </data>\n  <data name=\"SmtpPort\" xml:space=\"preserve\">\n    <value>Port (Standardwert ist 25)</value>\n  </data>\n  <data name=\"SmtpServer\" xml:space=\"preserve\">\n    <value>SMTP Server</value>\n  </data>\n  <data name=\"SmtpUser\" xml:space=\"preserve\">\n    <value>SMTP User</value>\n  </data>\n  <data name=\"StartDate\" xml:space=\"preserve\">\n    <value>Tag starten</value>\n  </data>\n  <data name=\"SubmitNewTopic\" xml:space=\"preserve\">\n    <value>Neues Thema hinzufügen</value>\n  </data>\n  <data name=\"SubmitReply\" xml:space=\"preserve\">\n    <value>Anwort miteilen</value>\n  </data>\n  <data name=\"TopicID\" xml:space=\"preserve\">\n    <value>AnwortID</value>\n  </data>\n  <data name=\"Type\" xml:space=\"preserve\">\n    <value>Type</value>\n  </data>\n  <data name=\"UseEsmtpCred\" xml:space=\"preserve\">\n    <value>Benutzen Sie ESMTP für Bescheinigungen</value>\n  </data>\n  <data name=\"UserID\" xml:space=\"preserve\">\n    <value>UserID</value>\n  </data>\n  <data name=\"UserIDTarget\" xml:space=\"preserve\">\n    <value>Ziel TeilnehmerID</value>\n  </data>\n  <data name=\"UserImageApprovalNotReq\" xml:space=\"preserve\">\n    <value>upgeloadete Mitglieder-Bilder benötigen im Moment keine Überprüfung</value>\n  </data>\n  <data name=\"UseSsl\" xml:space=\"preserve\">\n    <value>Use SSL</value>\n  </data>\n  <data name=\"ViewingRoles\" xml:space=\"preserve\">\n    <value>angezeigte Rollen</value>\n  </data>\n  <data name=\"Close\" xml:space=\"preserve\">\n    <value>Schließen</value>\n  </data>\n  <data name=\"CloseOnReply\" xml:space=\"preserve\">\n    <value>Schließen auf Antwort</value>\n  </data>\n  <data name=\"FavoriteMake\" xml:space=\"preserve\">\n    <value>Favorit erstellen</value>\n  </data>\n  <data name=\"FavoriteRemove\" xml:space=\"preserve\">\n    <value>Verschieben von Favorites</value>\n  </data>\n  <data name=\"Link\" xml:space=\"preserve\">\n    <value>Link</value>\n  </data>\n  <data name=\"Moderator\" xml:space=\"preserve\">\n    <value>Moderator</value>\n  </data>\n  <data name=\"NameAvatar\" xml:space=\"preserve\">\n    <value>{0}s avatar</value>\n    <comment>{0} ist ein Name</comment>\n  </data>\n  <data name=\"NameLastEdit\" xml:space=\"preserve\">\n    <value>zuletzt bearbeitet von {0}</value>\n    <comment>{0} ist ein Name</comment>\n  </data>\n  <data name=\"Open\" xml:space=\"preserve\">\n    <value>Öffnen</value>\n  </data>\n  <data name=\"Pin\" xml:space=\"preserve\">\n    <value>Pin</value>\n  </data>\n  <data name=\"PostReply\" xml:space=\"preserve\">\n    <value>Anwort senden</value>\n  </data>\n  <data name=\"Quote\" xml:space=\"preserve\">\n    <value>Quote</value>\n  </data>\n  <data name=\"RecentTopics\" xml:space=\"preserve\">\n    <value>neueste Themen</value>\n  </data>\n  <data name=\"Reply\" xml:space=\"preserve\">\n    <value>Antwort</value>\n  </data>\n  <data name=\"ShowMorePosts\" xml:space=\"preserve\">\n    <value>Mehr Einträge anzeigen</value>\n  </data>\n  <data name=\"ShowPreviousPosts\" xml:space=\"preserve\">\n    <value>zeige vorhergende Einträge</value>\n  </data>\n  <data name=\"Subscribe\" xml:space=\"preserve\">\n    <value>Abbonieren (Eintragen)</value>\n  </data>\n  <data name=\"Undelete\" xml:space=\"preserve\">\n    <value>Wiederherstellen</value>\n  </data>\n  <data name=\"Unpin\" xml:space=\"preserve\">\n    <value>Losheften</value>\n  </data>\n  <data name=\"Update\" xml:space=\"preserve\">\n    <value>Update</value>\n  </data>\n  <data name=\"Archive\" xml:space=\"preserve\">\n    <value>Archiv</value>\n  </data>\n  <data name=\"NewPM\" xml:space=\"preserve\">\n    <value>Neue Private Nachricht</value>\n  </data>\n  <data name=\"NoResults\" xml:space=\"preserve\">\n    <value>Keine Ergebnisse gefunden No results found</value>\n  </data>\n  <data name=\"Send\" xml:space=\"preserve\">\n    <value>Senden</value>\n  </data>\n  <data name=\"Unarchive\" xml:space=\"preserve\">\n    <value>dearchivieren</value>\n  </data>\n  <data name=\"ViewArchivedMessages\" xml:space=\"preserve\">\n    <value>Zeige archivierte Nachrichten</value>\n  </data>\n  <data name=\"DisplayName\" xml:space=\"preserve\">\n    <value>Namen einblenden</value>\n  </data>\n  <data name=\"Error\" xml:space=\"preserve\">\n    <value>Fehler</value>\n  </data>\n  <data name=\"ErrorSettingUpDb\" xml:space=\"preserve\">\n    <value>Es gab einen Fehler beim Einrichten der Datenbank.</value>\n  </data>\n  <data name=\"ForumReady\" xml:space=\"preserve\">\n    <value>Ihr Forum ist fertig.</value>\n  </data>\n  <data name=\"ForumTitle\" xml:space=\"preserve\">\n    <value>Forum Titel</value>\n  </data>\n  <data name=\"GoAdmin\" xml:space=\"preserve\">\n    <value>Gehen Sie in den Admin-Bereich um das Einrichten des Forum abzuschließen.</value>\n  </data>\n  <data name=\"InvalidLink\" xml:space=\"preserve\">\n    <value>Dieser Link ist nicht mehr gültig.</value>\n  </data>\n  <data name=\"NoDataConnection\" xml:space=\"preserve\">\n    <value>Keine Datenverbindung</value>\n  </data>\n  <data name=\"PopForumsSetup\" xml:space=\"preserve\">\n    <value>POP Forums Setup</value>\n  </data>\n  <data name=\"SetupCantConnect\" xml:space=\"preserve\">\n    <value>Das Setup hat keine Verbindung zum Data Store. Bitte überprüfen Sie den Connection String und Configuration.</value>\n  </data>\n  <data name=\"SetupConnSuccess\" xml:space=\"preserve\">\n    <value>Eine Datenbankverbindung war erfolgreich. Bitte füllen Sie die folgenden Werte aus, um zu starten.</value>\n  </data>\n  <data name=\"SetupDatabase\" xml:space=\"preserve\">\n    <value>Setup der Datenbank.</value>\n  </data>\n  <data name=\"SetupFirstUser\" xml:space=\"preserve\">\n    <value>Setup das erste Mitglied des Forms (wird Admin- und Moderator-Rechte bewilligen) Setup your first user (will be granted admin and moderator rights)</value>\n  </data>\n  <data name=\"SubscribedTopics\" xml:space=\"preserve\">\n    <value>Abbonierte Themen</value>\n  </data>\n  <data name=\"SubscribeLoggedIn\" xml:space=\"preserve\">\n    <value>Sie müssen eingeloggt sein um abbonierte Themen zu sehen.</value>\n  </data>\n  <data name=\"SubscribeNone\" xml:space=\"preserve\">\n    <value>Sie sind nicht abboniert zu einem Thema.</value>\n  </data>\n  <data name=\"Success\" xml:space=\"preserve\">\n    <value>Das war erfolgreich!</value>\n  </data>\n  <data name=\"UnsubscribeTopic\" xml:space=\"preserve\">\n    <value>{0} ist ausgetragen von {1}</value>\n    <comment>{0} ist ein Name, {1} ist ein Überschriften Titel</comment>\n  </data>\n  <data name=\"ArchivedNewPosts\" xml:space=\"preserve\">\n    <value>Archiviert mit neuen Einträgen (Posts)</value>\n  </data>\n  <data name=\"Closed\" xml:space=\"preserve\">\n    <value>geschlossenes Thema</value>\n  </data>\n  <data name=\"ClosedPinned\" xml:space=\"preserve\">\n    <value>geschlossen, verstiftetes (pinned) Thema</value>\n  </data>\n  <data name=\"DeleteTopic\" xml:space=\"preserve\">\n    <value>Lösche Thema</value>\n  </data>\n  <data name=\"First\" xml:space=\"preserve\">\n    <value>Erste</value>\n  </data>\n  <data name=\"ForgotPasswordEmail\" xml:space=\"preserve\">\n    <value>Um Ihr Passwort zu erneuern auf {0}, folgen Sie bitte diesem Link :{3}{1}{3}{3}{2}</value>\n    <comment>{0}=title of forum, {1}=email link, {2}=signature, {3}=new line</comment>\n  </data>\n  <data name=\"ForgotPasswordSubject\" xml:space=\"preserve\">\n    <value>Dein Passwort zurücksetzen Anleitung für {0}</value>\n    <comment>{0} ist Name des Forums</comment>\n  </data>\n  <data name=\"ForumNoPost\" xml:space=\"preserve\">\n    <value>Du hast keine Erlaubnis in dieses Forum zu posten.</value>\n  </data>\n  <data name=\"ForumNoView\" xml:space=\"preserve\">\n    <value>Du hast keine Erlaubnis dieses Forum zu besuchen.</value>\n  </data>\n  <data name=\"LessThanMinute\" xml:space=\"preserve\">\n    <value>Weniger als eine Minute zurück.</value>\n  </data>\n  <data name=\"LoginToPost\" xml:space=\"preserve\">\n    <value>Sie müssen eingeloggt sein um zu posten</value>\n  </data>\n  <data name=\"MinutesAgo\" xml:space=\"preserve\">\n    <value>Vor {0} Minuten</value>\n    <comment>{0} ist eine Nummer</comment>\n  </data>\n  <data name=\"More\" xml:space=\"preserve\">\n    <value>Mehr</value>\n  </data>\n  <data name=\"NewPosts\" xml:space=\"preserve\">\n    <value>Neue Posts</value>\n  </data>\n  <data name=\"NewPostsClosed\" xml:space=\"preserve\">\n    <value>Neue Posts, geschlossenes Thema</value>\n  </data>\n  <data name=\"NewPostsClosedPinned\" xml:space=\"preserve\">\n    <value>Neue Posts, geschlossenes; gemerktes Thema</value>\n  </data>\n  <data name=\"NewPostsPinned\" xml:space=\"preserve\">\n    <value>Neue Posts, gemerktes Thema</value>\n  </data>\n  <data name=\"Next\" xml:space=\"preserve\">\n    <value>Nächstes</value>\n  </data>\n  <data name=\"NoNewPosts\" xml:space=\"preserve\">\n    <value>Keine neuen Beiträge</value>\n  </data>\n  <data name=\"NotLoggedIn\" xml:space=\"preserve\">\n    <value>Sie sind nicht eingeloggt</value>\n  </data>\n  <data name=\"OneMinuteAgo\" xml:space=\"preserve\">\n    <value>1 Minute zurück</value>\n  </data>\n  <data name=\"Pinned\" xml:space=\"preserve\">\n    <value>gemerktes Thema</value>\n  </data>\n  <data name=\"PMCreateWarnings\" xml:space=\"preserve\">\n\t  <value>Muss beinhalten ein Nachricht und zuletzt ein Mitglied.</value>\n  </data>\n  <data name=\"PostEmpty\" xml:space=\"preserve\">\n    <value>Ein leerer Beitrag kann nicht erstellt werden.</value>\n  </data>\n  <data name=\"PostWait\" xml:space=\"preserve\">\n    <value>Du musst {0} Sekunden zwischen einem Beitrageintrag warten damit der letzte Eintrag nicht doppelt erscheint.</value>\n    <comment>{0} is a number for seconds</comment>\n  </data>\n  <data name=\"Previous\" xml:space=\"preserve\">\n    <value>Vorheriger</value>\n  </data>\n  <data name=\"RegisterEmailSubject\" xml:space=\"preserve\">\n    <value>Forum Registrierungen {0}</value>\n    <comment>{0} ist der Name des Forums</comment>\n  </data>\n  <data name=\"RegisterEmailThankYou\" xml:space=\"preserve\">\n    <value>Danke für die Registrierung mit {0}.{2}{2}{1}</value>\n    <comment>{0}=name of forum, {1}=forum link, {2}=new line</comment>\n  </data>\n  <data name=\"RegisterEmailThankYouVerify\" xml:space=\"preserve\">\n    <value>Danke für die Registrierung mit {0}. Bitte folgen Sie diesem Link um Ihre Registrierung zu bestätigen:{5}{5}{1} {5}{5} Wenn Sie diesem Link nicht folgen können, rufen Sie bitte die nachfolgende Seite auf, kopieren den \nBestätigungs-Code und fügen ihn ein: {5}{5}Link: {2} {5}Code: {3} {5}{5}{4}</value>\n    <comment>{0}=name of forum, {1}=auth link, {2}=verify link, {3}=auth code, {4}=sig, {5}new line</comment>\n  </data>\n  <data name=\"SettingsSaved\" xml:space=\"preserve\">\n    <value>Einstellungen sichern</value>\n  </data>\n  <data name=\"TodayTime\" xml:space=\"preserve\">\n    <value>Heute, {0}</value>\n    <comment>{0} ist eine Zeit wie 1:43pm{0} is a time like 1:43pm</comment>\n  </data>\n  <data name=\"TopicNotExist\" xml:space=\"preserve\">\n    <value>Diese Thema existiert nicht</value>\n  </data>\n  <data name=\"YesterdayTime\" xml:space=\"preserve\">\n    <value>Gestern, {0}</value>\n    <comment>{0} a time like 1:43pm</comment>\n  </data>\n  <data name=\"Loading\" xml:space=\"preserve\">\n    <value>wird geladen</value>\n  </data>\n  <data name=\"Voted\" xml:space=\"preserve\">\n    <value>Voted</value>\n  </data>\n  <data name=\"VoteUpPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; voted für einen Beitrag in dem Thema: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"None\" xml:space=\"preserve\">\n    <value>Keiner</value>\n  </data>\n  <data name=\"ScoringGame\" xml:space=\"preserve\">\n    <value>Scoring-Spiel</value>\n  </data>\n  <data name=\"ActivityFeed\" xml:space=\"preserve\">\n    <value>Aktivitäts-Feed</value>\n  </data>\n  <data name=\"NewReplyPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; einen Beitrag in dem Thema: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"NewPostPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; begann ein neues Thema: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to topic{3} is topic name</comment>\n  </data>\n  <data name=\"ExternalLogins\" xml:space=\"preserve\">\n    <value>Externe Logins</value>\n  </data>\n  <data name=\"UseExistingForumAccount\" xml:space=\"preserve\">\n    <value>Verwenden Sie vorhandene Forum angemeldet</value>\n  </data>\n  <data name=\"ExpiredLogin\" xml:space=\"preserve\">\n    <value>Das Login ist abgelaufen oder nicht gültig</value>\n  </data>\n  <data name=\"ExternalLoginsDisabled\" xml:space=\"preserve\">\n    <value>Externe Logins sind deaktiviert</value>\n  </data>\n  <data name=\"NoExternalLoginsRegistered\" xml:space=\"preserve\">\n    <value>Sie haben keine externen Logins registriert</value>\n  </data>\n  <data name=\"DeletePermanently\" xml:space=\"preserve\">\n    <value>Dauerhaft löschen</value>\n  </data>\n  <data name=\"Preview\" xml:space=\"preserve\">\n    <value>Vorschau</value>\n  </data>\n  <data name=\"AskAQuestion\" xml:space=\"preserve\">\n    <value>Stellen Sie eine Frage</value>\n  </data>\n  <data name=\"Answers\" xml:space=\"preserve\">\n    <value>Antworten</value>\n  </data>\n  <data name=\"ChooseAnswer\" xml:space=\"preserve\">\n    <value>Wählen Sie Antwort</value>\n  </data>\n  <data name=\"PostAnswer\" xml:space=\"preserve\">\n    <value>Beitrag Antwort</value>\n  </data>\n  <data name=\"QuestionAnswered\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; wählte eine Antwort auf die Frage: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"SubmitAnswer\" xml:space=\"preserve\">\n    <value>Antwort abschicken</value>\n  </data>\n  <data name=\"AppRestartRequired\" xml:space=\"preserve\">\n    <value>App Neustart erforderlich</value>\n  </data>\n  <data name=\"SearchError\" xml:space=\"preserve\">\n    <value>Es ist ein Suchfehler aufgetreten</value>\n  </data>\n  <data name=\"CloseOldTopics\" xml:space=\"preserve\">\n    <value>Schließen Sie alte Themen</value>\n  </data>\n  <data name=\"Days\" xml:space=\"preserve\">\n    <value>Tage</value>\n  </data>\n  <data name=\"PrivateForum\" xml:space=\"preserve\">\n    <value>Privates Forum</value>\n  </data>\n  <data name=\"AccountNotVerified\" xml:space=\"preserve\">\n    <value>Konto nicht verifiziert</value>\n  </data>\n  <data name=\"SelectText\" xml:space=\"preserve\">\n\t  <value>Text auswählen</value>\n  </data>\n  <data name=\"NewReplyNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; einen Beitrag in dem Thema: &lt;b&gt;{1}&lt;/b&gt;\n</value>\n  </data>\n  <data name=\"VoteUpNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; voted für einen Beitrag in dem Thema: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"QuestionAnsweredNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; wählte eine Antwort auf die Frage: &lt;b&gt;{1}&lt;/b&gt;\n</value>\n  </data>\n  <data name=\"AutoFollow\" xml:space=\"preserve\">\n\t  <value>Folgen Sie automatisch Themen, auf die Sie antworten</value>\n  </data>\n  <data name=\"MarkAllRead\" xml:space=\"preserve\">\n\t  <value>markiere alles Gelesene</value>\n  </data>\n  <data name=\"UploadImage\" xml:space=\"preserve\">\n\t  <value>Bild hochladen</value>\n  </data>\n  <data name=\"Ignored\" xml:space=\"preserve\">\n\t  <value>Ignoriert</value>\n  </data>\n  <data name=\"IgnoreList\" xml:space=\"preserve\">\n\t  <value>Ignorierliste</value>\n  </data>\n  <data name=\"Ignore\" xml:space=\"preserve\">\n\t  <value>Ignorieren</value>\n  </data>\n</root>"
  },
  {
    "path": "src/PopForums/Resources/Resources.es.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"Account\" xml:space=\"preserve\">\n    <value>Cuenta</value>\n  </data>\n  <data name=\"AccountCreated\" xml:space=\"preserve\">\n    <value>Cuenta Creada</value>\n  </data>\n  <data name=\"AccountReady\" xml:space=\"preserve\">\n    <value>Su cuenta fué creada, ya puede usar el foro.</value>\n  </data>\n  <data name=\"AccountReadyCheckEmail\" xml:space=\"preserve\">\n    <value>Revise su e-mail le ha sido enviado un link de verificación para usar el foro.</value>\n  </data>\n  <data name=\"AccountVerified\" xml:space=\"preserve\">\n    <value>Su cuenta ha sido verificada.</value>\n  </data>\n  <data name=\"AlreadyCreatedAccount\" xml:space=\"preserve\">\n    <value>Ud. ya está registrado, y tiene una cuenta válida.</value>\n  </data>\n  <data name=\"AsYouWouldLikeItToAppear\" xml:space=\"preserve\">\n    <value>(como ud. quiere que aparezca)</value>\n  </data>\n  <data name=\"AtLeastSixChar\" xml:space=\"preserve\">\n    <value>(mínimo 6 caracteres)</value>\n  </data>\n  <data name=\"By\" xml:space=\"preserve\">\n    <value>por</value>\n    <comment>context: \"Last post: 12am by Jeff\"</comment>\n  </data>\n  <data name=\"CreateAccountButton\" xml:space=\"preserve\">\n    <value>Crear cuenta</value>\n  </data>\n  <data name=\"CreateAnAccount\" xml:space=\"preserve\">\n    <value>Crear una cuenta</value>\n  </data>\n  <data name=\"EditYourProfile\" xml:space=\"preserve\">\n    <value>Editar su perfil</value>\n  </data>\n  <data name=\"Email\" xml:space=\"preserve\">\n    <value>E-mail</value>\n  </data>\n  <data name=\"EmailProblemAccount\" xml:space=\"preserve\">\n    <value>Hubo un problema enviando el e-mail de verificación: </value>\n  </data>\n  <data name=\"Favorites\" xml:space=\"preserve\">\n    <value>Favoritos</value>\n  </data>\n  <data name=\"Forbidden\" xml:space=\"preserve\">\n    <value>Lo sentimos, no tiene permisos para ver esto.</value>\n  </data>\n  <data name=\"Forums\" xml:space=\"preserve\">\n    <value>Foros</value>\n  </data>\n  <data name=\"HaveReadTOS\" xml:space=\"preserve\">\n    <value>He leido y acepto los términos de servicio.</value>\n  </data>\n  <data name=\"Last\" xml:space=\"preserve\">\n    <value>Último</value>\n  </data>\n  <data name=\"Login\" xml:space=\"preserve\">\n    <value>Ingreso</value>\n  </data>\n  <data name=\"Logout\" xml:space=\"preserve\">\n    <value>Salida</value>\n  </data>\n  <data name=\"MarkAllForumsRead\" xml:space=\"preserve\">\n\t  <value>Marcar todos como leidos</value>\n  </data>\n  <data name=\"MyBirthdayIsOnOrBefore\" xml:space=\"preserve\">\n    <value>Mi cumpleaños es antes de {0}</value>\n    <comment>where {0} is a date</comment>\n  </data>\n  <data name=\"Name\" xml:space=\"preserve\">\n    <value>Nombre</value>\n  </data>\n  <data name=\"NeedToVerifyExistingAccount\" xml:space=\"preserve\">\n    <value>Necesita verificar una cuenta existente?</value>\n  </data>\n  <data name=\"PageNotFound\" xml:space=\"preserve\">\n    <value>Página no encontrada</value>\n  </data>\n  <data name=\"Password\" xml:space=\"preserve\">\n    <value>Clave</value>\n  </data>\n  <data name=\"Posts\" xml:space=\"preserve\">\n    <value>Mensajes</value>\n  </data>\n  <data name=\"PrivateMessages\" xml:space=\"preserve\">\n    <value>Mensajes Privados</value>\n  </data>\n  <data name=\"Recent\" xml:space=\"preserve\">\n    <value>Recientes</value>\n  </data>\n  <data name=\"RegisteredUsers\" xml:space=\"preserve\">\n    <value>Usuarios registrados</value>\n  </data>\n  <data name=\"RetypePassword\" xml:space=\"preserve\">\n    <value>Reingrese la clave</value>\n  </data>\n  <data name=\"Search\" xml:space=\"preserve\">\n    <value>Buscar</value>\n  </data>\n  <data name=\"SubscribeToList\" xml:space=\"preserve\">\n    <value>Suscribirse a la lista de correo</value>\n  </data>\n  <data name=\"Subscriptions\" xml:space=\"preserve\">\n    <value>Suscripciones</value>\n  </data>\n  <data name=\"TermsOfService\" xml:space=\"preserve\">\n    <value>Términos de servicio</value>\n  </data>\n  <data name=\"TimeZone\" xml:space=\"preserve\">\n    <value>Zona Horaria</value>\n  </data>\n  <data name=\"Topics\" xml:space=\"preserve\">\n    <value>Tópicos</value>\n  </data>\n  <data name=\"Total\" xml:space=\"preserve\">\n    <value>Total</value>\n  </data>\n  <data name=\"TotalPosts\" xml:space=\"preserve\">\n    <value>Total mensajes</value>\n  </data>\n  <data name=\"TotalTopics\" xml:space=\"preserve\">\n    <value>Total topicos</value>\n  </data>\n  <data name=\"UseDaylight\" xml:space=\"preserve\">\n    <value>Use el horario de verano de N.America/Europa</value>\n  </data>\n  <data name=\"UsersOnline\" xml:space=\"preserve\">\n    <value>Usuarios en línea</value>\n  </data>\n  <data name=\"MustBe13\" xml:space=\"preserve\">\n    <value>Debe ser mayor de 13 años para registrarse.</value>\n  </data>\n  <data name=\"MustAcceptTOS\" xml:space=\"preserve\">\n    <value>Debe aceptar los términos de servicio</value>\n  </data>\n  <data name=\"RetypeYourPassword\" xml:space=\"preserve\">\n    <value>Reingrese su clave</value>\n  </data>\n  <data name=\"NameRequired\" xml:space=\"preserve\">\n    <value>Nombre requerido</value>\n  </data>\n  <data name=\"NameInUse\" xml:space=\"preserve\">\n    <value>Ese nombre ya ha sido usado</value>\n  </data>\n  <data name=\"EmailRequired\" xml:space=\"preserve\">\n    <value>E-mail requerido</value>\n  </data>\n  <data name=\"ValidEmailAddressRequired\" xml:space=\"preserve\">\n    <value>Se requiere un e-mail válido</value>\n  </data>\n  <data name=\"EmailInUse\" xml:space=\"preserve\">\n    <value>Ese e-mail ya ha sido usado</value>\n  </data>\n  <data name=\"EmailBanned\" xml:space=\"preserve\">\n    <value>Ese e-mail ha sido prohibido</value>\n  </data>\n  <data name=\"IPBanned\" xml:space=\"preserve\">\n    <value>Su dirección IP ha sido prohibida</value>\n  </data>\n  <data name=\"RetypePasswordMustMatch\" xml:space=\"preserve\">\n    <value>La clave debe coincidir con el campo anterior</value>\n  </data>\n  <data name=\"EditAccount\" xml:space=\"preserve\">\n    <value>Editar Cuenta</value>\n  </data>\n  <data name=\"EditProfile\" xml:space=\"preserve\">\n    <value>Editar Perfil</value>\n  </data>\n  <data name=\"EnterVerificationCode\" xml:space=\"preserve\">\n    <value>Por favor ingrese su codigo de verificación</value>\n  </data>\n  <data name=\"ForumHomePage\" xml:space=\"preserve\">\n    <value>Página Inicial de Foro</value>\n  </data>\n  <data name=\"MustBeRegisteredToEditAccount\" xml:space=\"preserve\">\n    <value>Ud. debe estar registrado y haber ingresado para editar su cuenta.</value>\n  </data>\n  <data name=\"NoUserFoundWithEmail\" xml:space=\"preserve\">\n    <value>No se encontro un uuario con ese e-mail</value>\n  </data>\n  <data name=\"SendEmailWithNewCodeButton\" xml:space=\"preserve\">\n    <value>Enviar e-mail con un nuevo código.</value>\n  </data>\n  <data name=\"VerificationEmailSent\" xml:space=\"preserve\">\n    <value>Su e-mail de verificación ha sido enviado.</value>\n  </data>\n  <data name=\"VerificationFailure\" xml:space=\"preserve\">\n    <value>Falla en la Verificación</value>\n  </data>\n  <data name=\"VerificationIfYouNeed\" xml:space=\"preserve\">\n    <value>Si necesita un nuevo código de verificación, ingrese su e-mail aqui</value>\n  </data>\n  <data name=\"VerificationLinkBad\" xml:space=\"preserve\">\n    <value>Lo sentimos, el link de verificación o el código que ingreso son inválidos.</value>\n  </data>\n  <data name=\"VerifyAccount\" xml:space=\"preserve\">\n    <value>Verificar Cuenta</value>\n  </data>\n  <data name=\"VerifyCodeButton\" xml:space=\"preserve\">\n    <value>Verificar Código</value>\n  </data>\n  <data name=\"Avatar\" xml:space=\"preserve\">\n    <value>Avatar</value>\n  </data>\n  <data name=\"AvatarDelete\" xml:space=\"preserve\">\n    <value>Borrar este avatar</value>\n  </data>\n  <data name=\"ChangeEmail\" xml:space=\"preserve\">\n    <value>Cambiar e-mail</value>\n  </data>\n  <data name=\"ChangeEmailButton\" xml:space=\"preserve\">\n    <value>Cambiar e-mail y enviar código</value>\n  </data>\n  <data name=\"ChangeEmailConsequence\" xml:space=\"preserve\">\n    <value>El cambio de el e-mail requiere que se genere un nuevo codigo de verificación y que se envie al nuevo e-mail. No podrá colocar mensajes hasta que no verifique su e-mail con el nuevo código.</value>\n  </data>\n  <data name=\"ChangePassword\" xml:space=\"preserve\">\n    <value>Cambiar Clave</value>\n  </data>\n  <data name=\"ChangeYourEmailPassword\" xml:space=\"preserve\">\n    <value>Cambiar su e-mail o su clave</value>\n  </data>\n  <data name=\"DateOfBirth\" xml:space=\"preserve\">\n    <value>Fecha de Nacimiento</value>\n  </data>\n  <data name=\"Details\" xml:space=\"preserve\">\n    <value>Detalles</value>\n  </data>\n  <data name=\"EmailChangeSuccess\" xml:space=\"preserve\">\n    <value>Su e-mail fué cambiado</value>\n  </data>\n  <data name=\"EmailNew\" xml:space=\"preserve\">\n    <value>Nuevo e-mail</value>\n  </data>\n  <data name=\"EmailNewRetype\" xml:space=\"preserve\">\n    <value>Reingrese el nuevo e-mail</value>\n  </data>\n  <data name=\"EmailNotFound\" xml:space=\"preserve\">\n    <value>E-mail no encontrado</value>\n  </data>\n  <data name=\"EmailSent\" xml:space=\"preserve\">\n    <value>Su e-mail ha sido enviado</value>\n  </data>\n  <data name=\"EmailsMustMatch\" xml:space=\"preserve\">\n    <value>Las direcciones de e'mail no coinciden</value>\n  </data>\n  <data name=\"EmailUser\" xml:space=\"preserve\">\n    <value>E-mail de Usuario</value>\n  </data>\n  <data name=\"ForcePlainTextBox\" xml:space=\"preserve\">\n    <value>Forzar recuadros de texto planos.</value>\n  </data>\n  <data name=\"ForgotInstructions\" xml:space=\"preserve\">\n    <value>Ingrese el e-mail asociado a su cuenta para recibir instrucciones para resetear su clave:</value>\n  </data>\n  <data name=\"ForgotInstructionsSent\" xml:space=\"preserve\">\n    <value>Instrucciones enviadas a su e-mail</value>\n  </data>\n  <data name=\"ForgotPassword\" xml:space=\"preserve\">\n    <value>Clave olvidada</value>\n  </data>\n  <data name=\"ForgotPasswordQuestion\" xml:space=\"preserve\">\n    <value>Olvidó su clave?</value>\n  </data>\n  <data name=\"HideVanity\" xml:space=\"preserve\">\n    <value>Ocultar Vanity</value>\n  </data>\n  <data name=\"In\" xml:space=\"preserve\">\n    <value>en</value>\n    <comment>context: started by Jeff in General Forum</comment>\n  </data>\n  <data name=\"Joined\" xml:space=\"preserve\">\n    <value>Unido(s)</value>\n  </data>\n  <data name=\"Location\" xml:space=\"preserve\">\n    <value>Ubicación</value>\n  </data>\n  <data name=\"LoginAlready\" xml:space=\"preserve\">\n    <value>Ud. ya ha ingresado.</value>\n  </data>\n  <data name=\"LoginBad\" xml:space=\"preserve\">\n    <value>E-mail o clave erróneos</value>\n  </data>\n  <data name=\"ManagePhotos\" xml:space=\"preserve\">\n    <value>Administrar fotos</value>\n  </data>\n  <data name=\"ManageYourPhotos\" xml:space=\"preserve\">\n    <value>Administrar sus fotos</value>\n  </data>\n  <data name=\"NewPasswordSaved\" xml:space=\"preserve\">\n    <value>Nueva clave grabada</value>\n  </data>\n  <data name=\"NotRegisteredQuestion\" xml:space=\"preserve\">\n    <value>No está registrado?</value>\n  </data>\n  <data name=\"OldPasswordIncorrect\" xml:space=\"preserve\">\n    <value>Clave antigua incorrecta</value>\n  </data>\n  <data name=\"Options\" xml:space=\"preserve\">\n    <value>Opciones</value>\n  </data>\n  <data name=\"PasswordNew\" xml:space=\"preserve\">\n    <value>Nueva clave</value>\n  </data>\n  <data name=\"PasswordNewRetype\" xml:space=\"preserve\">\n    <value>Reingrese nueva clave</value>\n  </data>\n  <data name=\"PasswordOld\" xml:space=\"preserve\">\n    <value>Clave anterior</value>\n  </data>\n  <data name=\"PasswordReset\" xml:space=\"preserve\">\n    <value>Resetear clave</value>\n  </data>\n  <data name=\"PasswordResetLinkInvalid\" xml:space=\"preserve\">\n    <value>Este no es un vinculo válido para resetear la clave.</value>\n  </data>\n  <data name=\"PasswordResetNote\" xml:space=\"preserve\">\n    <value>Su nueva clave ya está activa, ya ha ingresado al foro.</value>\n  </data>\n  <data name=\"PasswordResetSuccess\" xml:space=\"preserve\">\n    <value>Reseteo de clave satisfactorio</value>\n  </data>\n  <data name=\"Photo\" xml:space=\"preserve\">\n    <value>Foto</value>\n  </data>\n  <data name=\"PhotoDelete\" xml:space=\"preserve\">\n    <value>Borrar esta foto</value>\n  </data>\n  <data name=\"PhotoNotApproved\" xml:space=\"preserve\">\n    <value>(esta imagen aún no ha sido aprobada por un administrador)</value>\n  </data>\n  <data name=\"ProfileUpdated\" xml:space=\"preserve\">\n    <value>Perfil actualizado</value>\n  </data>\n  <data name=\"RememberMe\" xml:space=\"preserve\">\n    <value>Recordarme?</value>\n  </data>\n  <data name=\"Replies\" xml:space=\"preserve\">\n    <value>Respuestas</value>\n  </data>\n  <data name=\"Save\" xml:space=\"preserve\">\n    <value>Grabar</value>\n  </data>\n  <data name=\"SendEmailButton\" xml:space=\"preserve\">\n    <value>Enviar E-mail</value>\n  </data>\n  <data name=\"SendPM\" xml:space=\"preserve\">\n    <value>Enviar Mensaje Privado</value>\n  </data>\n  <data name=\"ShowProfileDetails\" xml:space=\"preserve\">\n    <value>Mostrar los detalles de su perfil (pero no su e-mail)</value>\n  </data>\n  <data name=\"Signature\" xml:space=\"preserve\">\n    <value>Firma</value>\n  </data>\n  <data name=\"StartedBy\" xml:space=\"preserve\">\n    <value>Iniciado por</value>\n  </data>\n  <data name=\"Subject\" xml:space=\"preserve\">\n    <value>Asunto</value>\n  </data>\n  <data name=\"Text\" xml:space=\"preserve\">\n    <value>Texto</value>\n  </data>\n  <data name=\"To\" xml:space=\"preserve\">\n    <value>Para</value>\n  </data>\n  <data name=\"UploadNew\" xml:space=\"preserve\">\n    <value>Crgar nuevo</value>\n  </data>\n  <data name=\"UserNotFound\" xml:space=\"preserve\">\n    <value>Usuario no encontrado</value>\n  </data>\n  <data name=\"Views\" xml:space=\"preserve\">\n    <value>Vistas</value>\n  </data>\n  <data name=\"Web\" xml:space=\"preserve\">\n    <value>Web</value>\n  </data>\n  <data name=\"WebVisit\" xml:space=\"preserve\">\n    <value>Visitar nuestro sitio Web</value>\n  </data>\n  <data name=\"YourIP\" xml:space=\"preserve\">\n    <value>Su direccion IP</value>\n  </data>\n  <data name=\"Birthday\" xml:space=\"preserve\">\n    <value>Cumpleaños</value>\n  </data>\n  <data name=\"Contact\" xml:space=\"preserve\">\n    <value>Contacto</value>\n  </data>\n  <data name=\"Profile\" xml:space=\"preserve\">\n    <value>Perfil</value>\n  </data>\n  <data name=\"SendNameEmail\" xml:space=\"preserve\">\n    <value>Enviar a {0} un e-mail</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"SendNamePM\" xml:space=\"preserve\">\n    <value>Enviar a {0} un mensaje privado</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Unsubscribe\" xml:space=\"preserve\">\n    <value>Cancelar suscripción</value>\n  </data>\n  <data name=\"UnsubscribeFail\" xml:space=\"preserve\">\n    <value>Falla en cancelación de suscripción</value>\n  </data>\n  <data name=\"UnsubscribeLinkBad\" xml:space=\"preserve\">\n    <value>El enlace que ud. siguió no es válido.</value>\n  </data>\n  <data name=\"UnsubscribeNote\" xml:space=\"preserve\">\n    <value>Su solicitud de cancelación de suscripción ha sido procesada, Puede suscribisrse de nuevo ingresando y visitando la página de Cuenta.</value>\n  </data>\n  <data name=\"AddForum\" xml:space=\"preserve\">\n    <value>Adicionar Foro</value>\n  </data>\n  <data name=\"AddNew\" xml:space=\"preserve\">\n    <value>Adicionar Nuevo</value>\n  </data>\n  <data name=\"AllowImages\" xml:space=\"preserve\">\n    <value>Permitir Imégenes</value>\n  </data>\n  <data name=\"Archived\" xml:space=\"preserve\">\n    <value>Archivado</value>\n  </data>\n  <data name=\"AreYouSure\" xml:space=\"preserve\">\n    <value>Está seguro?</value>\n  </data>\n  <data name=\"Ban\" xml:space=\"preserve\">\n    <value>Bloquear</value>\n  </data>\n  <data name=\"BanRemove\" xml:space=\"preserve\">\n    <value>Remover Bloqueo</value>\n  </data>\n  <data name=\"Categories\" xml:space=\"preserve\">\n    <value>Categorias</value>\n  </data>\n  <data name=\"Category\" xml:space=\"preserve\">\n    <value>Categoría</value>\n  </data>\n  <data name=\"CategoryEditTitle\" xml:space=\"preserve\">\n    <value>Editar Título de Categoría</value>\n  </data>\n  <data name=\"CategoryMoveError\" xml:space=\"preserve\">\n    <value>Se produjo un error desconocido al intentar mover la categoría</value>\n  </data>\n  <data name=\"CensorReplacementChar\" xml:space=\"preserve\">\n    <value>Caracter de reemplazo de Censura</value>\n  </data>\n  <data name=\"CensorWords\" xml:space=\"preserve\">\n    <value>Palabras Censuradas</value>\n  </data>\n  <data name=\"Delete\" xml:space=\"preserve\">\n    <value>Borrar</value>\n  </data>\n  <data name=\"DeleteAndBanButton\" xml:space=\"preserve\">\n    <value>Borrar y Bloquear Usuario</value>\n  </data>\n  <data name=\"DeleteUserButton\" xml:space=\"preserve\">\n    <value>Borrar Usuario</value>\n  </data>\n  <data name=\"Description\" xml:space=\"preserve\">\n    <value>Descripción</value>\n  </data>\n  <data name=\"Down\" xml:space=\"preserve\">\n    <value>Abajo</value>\n  </data>\n  <data name=\"Edit\" xml:space=\"preserve\">\n    <value>Editar</value>\n  </data>\n  <data name=\"EditForum\" xml:space=\"preserve\">\n    <value>Editar Foro</value>\n  </data>\n  <data name=\"EditUser\" xml:space=\"preserve\">\n    <value>Editar Usuario</value>\n  </data>\n  <data name=\"EmailBan\" xml:space=\"preserve\">\n    <value>Bloquear E-mail</value>\n  </data>\n  <data name=\"EmailIpBan\" xml:space=\"preserve\">\n    <value>Bloquear E-mail/IP </value>\n  </data>\n  <data name=\"EmailNewOptional\" xml:space=\"preserve\">\n    <value>Nuevo e-mail (opcional)</value>\n  </data>\n  <data name=\"EmailUsers\" xml:space=\"preserve\">\n    <value>Enviar E-mail a Usuarios</value>\n  </data>\n  <data name=\"ErrorLog\" xml:space=\"preserve\">\n    <value>Bitácora de Errores</value>\n  </data>\n  <data name=\"Forum\" xml:space=\"preserve\">\n    <value>Foro</value>\n  </data>\n  <data name=\"ForumAdapter\" xml:space=\"preserve\">\n    <value>Adaptador del Foro (opcional, use \"Namespace.Type, AssemblyName\")</value>\n  </data>\n  <data name=\"ForumHome\" xml:space=\"preserve\">\n    <value>Página Principal del Foro</value>\n  </data>\n  <data name=\"ForumPermisions\" xml:space=\"preserve\">\n    <value>Permisos del Foro</value>\n  </data>\n  <data name=\"ForumSettings\" xml:space=\"preserve\">\n    <value>Configuración del Foro</value>\n  </data>\n  <data name=\"ForumsUncat\" xml:space=\"preserve\">\n    <value>Foros sin categoría</value>\n  </data>\n  <data name=\"GeneralSettings\" xml:space=\"preserve\">\n    <value>Configuración General</value>\n  </data>\n  <data name=\"IpBan\" xml:space=\"preserve\">\n    <value>Bloquear IP</value>\n  </data>\n  <data name=\"IpHistory\" xml:space=\"preserve\">\n    <value>Historico de  IP</value>\n  </data>\n  <data name=\"IsApproved\" xml:space=\"preserve\">\n    <value>Aprobado</value>\n  </data>\n  <data name=\"IsSubscribed\" xml:space=\"preserve\">\n    <value>Suscrito</value>\n  </data>\n  <data name=\"LogErrors\" xml:space=\"preserve\">\n    <value>Registrar errores</value>\n  </data>\n  <data name=\"Logging\" xml:space=\"preserve\">\n    <value>Ingresando</value>\n  </data>\n  <data name=\"LogMod\" xml:space=\"preserve\">\n    <value>Moderar Registro</value>\n  </data>\n  <data name=\"LogSecurity\" xml:space=\"preserve\">\n    <value>Acciones de Seguridad del Registro</value>\n  </data>\n  <data name=\"MinimumTimeBetweenPosts\" xml:space=\"preserve\">\n    <value>Tiempo Mínimo entre mensajes (segundos)</value>\n  </data>\n  <data name=\"ModerationLog\" xml:space=\"preserve\">\n    <value>Moderar Registro</value>\n  </data>\n  <data name=\"Move\" xml:space=\"preserve\">\n    <value>Mover</value>\n  </data>\n  <data name=\"NewUserApprovedWithoutVerification\" xml:space=\"preserve\">\n    <value>Nuevo usuario aprobado sin verificación</value>\n  </data>\n  <data name=\"NewUserImageApprovedWithoutMod\" xml:space=\"preserve\">\n    <value>Nueva imagen de usuario aprobada sin moderación.</value>\n  </data>\n  <data name=\"Parsing\" xml:space=\"preserve\">\n    <value>Analizando</value>\n  </data>\n  <data name=\"PasswordNewOptional\" xml:space=\"preserve\">\n    <value>Nueva Clave (optional)</value>\n  </data>\n  <data name=\"PopForumsAdmin\" xml:space=\"preserve\">\n    <value>Administracion de foros POP</value>\n  </data>\n  <data name=\"PostsPerPage\" xml:space=\"preserve\">\n    <value>Mensajes por Página</value>\n  </data>\n  <data name=\"Role\" xml:space=\"preserve\">\n    <value>Rol</value>\n  </data>\n  <data name=\"Roles\" xml:space=\"preserve\">\n    <value>Roles</value>\n  </data>\n  <data name=\"Security\" xml:space=\"preserve\">\n    <value>Seguridad</value>\n  </data>\n  <data name=\"SecurityLog\" xml:space=\"preserve\">\n    <value>Registro de Seguridad</value>\n  </data>\n  <data name=\"ServerTime\" xml:space=\"preserve\">\n    <value>Hora del Servidor</value>\n  </data>\n  <data name=\"Services\" xml:space=\"preserve\">\n    <value>Servicios</value>\n  </data>\n  <data name=\"SessionLength\" xml:space=\"preserve\">\n    <value>Largo de la Sesión (minutos)</value>\n  </data>\n  <data name=\"Size\" xml:space=\"preserve\">\n    <value>Tamaño</value>\n  </data>\n  <data name=\"SubjectAndBodyNotEmpty\" xml:space=\"preserve\">\n    <value>El asunto y el detalle no deben ir vacios</value>\n  </data>\n  <data name=\"Title\" xml:space=\"preserve\">\n    <value>Titulo</value>\n  </data>\n  <data name=\"TopicsPerPage\" xml:space=\"preserve\">\n    <value>Tópicos por página</value>\n  </data>\n  <data name=\"Up\" xml:space=\"preserve\">\n    <value>Arriba</value>\n  </data>\n  <data name=\"UserAvatarMaxDim\" xml:space=\"preserve\">\n    <value>Dimensiones máximas del Avatar</value>\n  </data>\n  <data name=\"UserDeleteWarning\" xml:space=\"preserve\">\n    <value>PRECAUCION! La eliminacin de una cuenta es permanente.</value>\n  </data>\n  <data name=\"UserImageApproval\" xml:space=\"preserve\">\n    <value>Aprobación de imagen de usuario</value>\n  </data>\n  <data name=\"UserImageMaxDim\" xml:space=\"preserve\">\n    <value>Dimensiones máximas de imagen de usuario</value>\n  </data>\n  <data name=\"UserRoles\" xml:space=\"preserve\">\n    <value>Roles de Usuario</value>\n  </data>\n  <data name=\"Visible\" xml:space=\"preserve\">\n    <value>Visible</value>\n  </data>\n  <data name=\"AddNewForum\" xml:space=\"preserve\">\n    <value>Crear nuevo Foro</value>\n  </data>\n  <data name=\"Body\" xml:space=\"preserve\">\n    <value>Detalle</value>\n  </data>\n  <data name=\"BodyHtml\" xml:space=\"preserve\">\n    <value>Detalle HTML</value>\n  </data>\n  <data name=\"ClickToLoadMorePosts\" xml:space=\"preserve\">\n    <value>Click para leer nuevos mensajes antes de responder</value>\n  </data>\n  <data name=\"Comment\" xml:space=\"preserve\">\n    <value>Comentario</value>\n  </data>\n  <data name=\"CommentsOptional\" xml:space=\"preserve\">\n    <value>Comentarios (opcional)</value>\n  </data>\n  <data name=\"CreateJunkWordButton\" xml:space=\"preserve\">\n    <value>Crear nueva palabra \"Basura\"</value>\n  </data>\n  <data name=\"CreateNewReply\" xml:space=\"preserve\">\n    <value>Responder</value>\n  </data>\n  <data name=\"CreateNewRole\" xml:space=\"preserve\">\n    <value>Crear nuevo Rol</value>\n  </data>\n  <data name=\"CreateNewTopic\" xml:space=\"preserve\">\n    <value>Crear su nuevo Tópico</value>\n  </data>\n  <data name=\"DeleteAllErrors\" xml:space=\"preserve\">\n    <value>Eliminar todos los errores</value>\n  </data>\n  <data name=\"DeleteJunkWordButton\" xml:space=\"preserve\">\n    <value>Eliminar palabra \"Basura\" seleccionada</value>\n  </data>\n  <data name=\"DeleteSelectedRole\" xml:space=\"preserve\">\n    <value>Eliminar Rol seleccionado</value>\n  </data>\n  <data name=\"EditPost\" xml:space=\"preserve\">\n    <value>Editar Mensaje</value>\n  </data>\n  <data name=\"EmailUsersQueued\" xml:space=\"preserve\">\n    <value>E-mail en cola para usuarios suscritos..</value>\n  </data>\n  <data name=\"EndDate\" xml:space=\"preserve\">\n    <value>Fecha final</value>\n  </data>\n  <data name=\"Event\" xml:space=\"preserve\">\n    <value>Evento</value>\n  </data>\n  <data name=\"EventTime\" xml:space=\"preserve\">\n    <value>Hora del Evento</value>\n  </data>\n  <data name=\"FavoriteMustBeLoggedIn\" xml:space=\"preserve\">\n    <value>Debe haber ingresado para ver tópicos favoritos.</value>\n  </data>\n  <data name=\"FavoritesDontHave\" xml:space=\"preserve\">\n    <value>No tiene tópicos favoritos.</value>\n  </data>\n  <data name=\"FavoriteTopics\" xml:space=\"preserve\">\n    <value>Tópicos Favoritos</value>\n  </data>\n  <data name=\"ForumMoveError\" xml:space=\"preserve\">\n    <value>Se presento un error al intentar mover el foro</value>\n  </data>\n  <data name=\"ForumPermissionInstructions\" xml:space=\"preserve\">\n    <value>Para remover  restricciones de mensajes y visibilidad, remueva los roles de la lista apropiada.</value>\n  </data>\n  <data name=\"FromEmailAddress\" xml:space=\"preserve\">\n    <value>E-mail de origen</value>\n  </data>\n  <data name=\"ID\" xml:space=\"preserve\">\n    <value>ID</value>\n  </data>\n  <data name=\"IncludeSignature\" xml:space=\"preserve\">\n    <value>Incluir firma</value>\n  </data>\n  <data name=\"IndexingInterval\" xml:space=\"preserve\">\n    <value>Intervalo de Indice</value>\n  </data>\n  <data name=\"IP\" xml:space=\"preserve\">\n    <value>IP</value>\n  </data>\n  <data name=\"IsRunning\" xml:space=\"preserve\">\n    <value>Ejecutando</value>\n  </data>\n  <data name=\"JunkWords\" xml:space=\"preserve\">\n    <value>Palabras \"Basura\"</value>\n  </data>\n  <data name=\"MarkForumRead\" xml:space=\"preserve\">\n    <value>Marcar foro como leido</value>\n  </data>\n  <data name=\"Message\" xml:space=\"preserve\">\n    <value>Mensaje</value>\n  </data>\n  <data name=\"Millseconds\" xml:space=\"preserve\">\n    <value>Milisegundos</value>\n  </data>\n  <data name=\"NamePosts\" xml:space=\"preserve\">\n    <value>Mensajes de {0}</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Optional\" xml:space=\"preserve\">\n    <value>(opcional)</value>\n  </data>\n  <data name=\"PermRoles\" xml:space=\"preserve\">\n    <value>Admin y Moderator son roles permanentes y no pueden ser eliminados.</value>\n  </data>\n  <data name=\"PostID\" xml:space=\"preserve\">\n    <value>ID de Mensaje</value>\n  </data>\n  <data name=\"PostingRoles\" xml:space=\"preserve\">\n    <value>Roles de Publicación</value>\n  </data>\n  <data name=\"PostNewTopic\" xml:space=\"preserve\">\n    <value>Publicar un nuevo tópico</value>\n  </data>\n  <data name=\"PreviewTopic\" xml:space=\"preserve\">\n    <value>Previsualizar  tópico</value>\n    <comment>preview the verb, not adjective</comment>\n  </data>\n  <data name=\"Remove\" xml:space=\"preserve\">\n    <value>Borrar</value>\n  </data>\n  <data name=\"RemoveAll\" xml:space=\"preserve\">\n    <value>Borrar Todo</value>\n  </data>\n  <data name=\"SendingInterval\" xml:space=\"preserve\">\n    <value>Intervalo de Envios</value>\n  </data>\n  <data name=\"SmtpPassword\" xml:space=\"preserve\">\n    <value>Clave de SMTP</value>\n  </data>\n  <data name=\"SmtpPort\" xml:space=\"preserve\">\n    <value>Puerto (default : 25)</value>\n  </data>\n  <data name=\"SmtpServer\" xml:space=\"preserve\">\n    <value>Servidor SMTP</value>\n  </data>\n  <data name=\"SmtpUser\" xml:space=\"preserve\">\n    <value>Usuario SMTP</value>\n  </data>\n  <data name=\"StartDate\" xml:space=\"preserve\">\n    <value>Fecha de Inicio</value>\n  </data>\n  <data name=\"SubmitNewTopic\" xml:space=\"preserve\">\n    <value>Enviar nuevo tópico</value>\n  </data>\n  <data name=\"SubmitReply\" xml:space=\"preserve\">\n    <value>Enviar respuesta</value>\n  </data>\n  <data name=\"TopicID\" xml:space=\"preserve\">\n    <value>ID de Tópico</value>\n  </data>\n  <data name=\"Type\" xml:space=\"preserve\">\n    <value>Tipo</value>\n  </data>\n  <data name=\"UseEsmtpCred\" xml:space=\"preserve\">\n    <value>Use ESMTP para las credenciales</value>\n  </data>\n  <data name=\"UserID\" xml:space=\"preserve\">\n    <value>ID de Usuario</value>\n  </data>\n  <data name=\"UserIDTarget\" xml:space=\"preserve\">\n    <value>ID de usuario de destino</value>\n  </data>\n  <data name=\"UserImageApprovalNotReq\" xml:space=\"preserve\">\n    <value>Las imagenes cargadas actualmente no requieren aprobación</value>\n  </data>\n  <data name=\"UseSsl\" xml:space=\"preserve\">\n    <value>Usar SSL</value>\n  </data>\n  <data name=\"ViewingRoles\" xml:space=\"preserve\">\n    <value>Roles de Visivilidad</value>\n  </data>\n  <data name=\"Close\" xml:space=\"preserve\">\n    <value>Cerrar</value>\n  </data>\n  <data name=\"CloseOnReply\" xml:space=\"preserve\">\n    <value>Cerrar ante una respuesta</value>\n  </data>\n  <data name=\"FavoriteMake\" xml:space=\"preserve\">\n    <value>Marcar como favorito</value>\n  </data>\n  <data name=\"FavoriteRemove\" xml:space=\"preserve\">\n    <value>Quitar de Favorites</value>\n  </data>\n  <data name=\"Link\" xml:space=\"preserve\">\n    <value>Enlace</value>\n  </data>\n  <data name=\"Moderator\" xml:space=\"preserve\">\n    <value>Moderador</value>\n  </data>\n  <data name=\"NameAvatar\" xml:space=\"preserve\">\n    <value>Avetar de {0}</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"NameLastEdit\" xml:space=\"preserve\">\n    <value>Editado por {0}</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Open\" xml:space=\"preserve\">\n    <value>Abrir</value>\n  </data>\n  <data name=\"Pin\" xml:space=\"preserve\">\n    <value>Anclar</value>\n  </data>\n  <data name=\"PostReply\" xml:space=\"preserve\">\n    <value>Publicar una respuesta</value>\n  </data>\n  <data name=\"Quote\" xml:space=\"preserve\">\n    <value>Cita</value>\n  </data>\n  <data name=\"RecentTopics\" xml:space=\"preserve\">\n    <value>Tópicos Recientes</value>\n  </data>\n  <data name=\"Reply\" xml:space=\"preserve\">\n    <value>Responder</value>\n  </data>\n  <data name=\"ShowMorePosts\" xml:space=\"preserve\">\n    <value>Mostrar más mensajes</value>\n  </data>\n  <data name=\"ShowPreviousPosts\" xml:space=\"preserve\">\n    <value>Mostrar mensajes anteriores</value>\n  </data>\n  <data name=\"Subscribe\" xml:space=\"preserve\">\n    <value>Subscribir</value>\n  </data>\n  <data name=\"Undelete\" xml:space=\"preserve\">\n    <value>Recuperar</value>\n  </data>\n  <data name=\"Unpin\" xml:space=\"preserve\">\n    <value>Desanclar</value>\n  </data>\n  <data name=\"Update\" xml:space=\"preserve\">\n    <value>Actualizar</value>\n  </data>\n  <data name=\"Archive\" xml:space=\"preserve\">\n    <value>Archivar</value>\n  </data>\n  <data name=\"NewPM\" xml:space=\"preserve\">\n    <value>Nuevo mensaje privado</value>\n  </data>\n  <data name=\"NoResults\" xml:space=\"preserve\">\n    <value>No se encontraron resultados</value>\n  </data>\n  <data name=\"Send\" xml:space=\"preserve\">\n    <value>Enviar</value>\n  </data>\n  <data name=\"Unarchive\" xml:space=\"preserve\">\n    <value>Desarchivar</value>\n  </data>\n  <data name=\"ViewArchivedMessages\" xml:space=\"preserve\">\n    <value>Ver mensajes archivados</value>\n  </data>\n  <data name=\"DisplayName\" xml:space=\"preserve\">\n    <value>Nombre para mostrar</value>\n  </data>\n  <data name=\"Error\" xml:space=\"preserve\">\n    <value>Error</value>\n  </data>\n  <data name=\"ErrorSettingUpDb\" xml:space=\"preserve\">\n    <value>Se presento un error al configurar la base de datos</value>\n  </data>\n  <data name=\"ForumReady\" xml:space=\"preserve\">\n    <value>Su foro está listo.</value>\n  </data>\n  <data name=\"ForumTitle\" xml:space=\"preserve\">\n    <value>Titulo del Foro</value>\n  </data>\n  <data name=\"GoAdmin\" xml:space=\"preserve\">\n    <value>Vaya a la seccion de Administración para terminar de configurar su foro.</value>\n  </data>\n  <data name=\"InvalidLink\" xml:space=\"preserve\">\n    <value>Ese enlace ya no es válido</value>\n  </data>\n  <data name=\"NoDataConnection\" xml:space=\"preserve\">\n    <value>No hay conección de datos</value>\n  </data>\n  <data name=\"PopForumsSetup\" xml:space=\"preserve\">\n    <value>Configuración de Foros POP</value>\n  </data>\n  <data name=\"SetupCantConnect\" xml:space=\"preserve\">\n    <value>El sistema no puede conectarse a la base de datos. Por favor verifique su cadena de conección y su configuración</value>\n  </data>\n  <data name=\"SetupConnSuccess\" xml:space=\"preserve\">\n    <value>Conección a la base de datos satisfactoria. Por favor  ingrese los siguientes valores para empezar.</value>\n  </data>\n  <data name=\"SetupDatabase\" xml:space=\"preserve\">\n    <value>Configure la base de datos</value>\n  </data>\n  <data name=\"SetupFirstUser\" xml:space=\"preserve\">\n    <value>Configura su primer usurio (le serán asignados privilegios de Admin y Moderator)</value>\n  </data>\n  <data name=\"SubscribedTopics\" xml:space=\"preserve\">\n    <value>Tópicos suscritos</value>\n  </data>\n  <data name=\"SubscribeLoggedIn\" xml:space=\"preserve\">\n    <value>Debe haber ingresado para ver tópicos suscritos.</value>\n  </data>\n  <data name=\"SubscribeNone\" xml:space=\"preserve\">\n    <value>No está suscrito a ningún tópico.</value>\n  </data>\n  <data name=\"Success\" xml:space=\"preserve\">\n    <value>Finalizado con éxito!</value>\n  </data>\n  <data name=\"UnsubscribeTopic\" xml:space=\"preserve\">\n    <value>{0} ya no está suscrito a {1}</value>\n    <comment>{0} is a name, {1} is a topic title</comment>\n  </data>\n  <data name=\"ArchivedNewPosts\" xml:space=\"preserve\">\n    <value>Archivado con nuevos mensajes</value>\n  </data>\n  <data name=\"Closed\" xml:space=\"preserve\">\n    <value>Tópico cerrado</value>\n  </data>\n  <data name=\"ClosedPinned\" xml:space=\"preserve\">\n    <value>Tópico anclado, ha sido cerrado.</value>\n  </data>\n  <data name=\"DeleteTopic\" xml:space=\"preserve\">\n    <value>Eliminar tópico</value>\n  </data>\n  <data name=\"First\" xml:space=\"preserve\">\n    <value>Primero</value>\n  </data>\n  <data name=\"ForgotPasswordEmail\" xml:space=\"preserve\">\n    <value>Para resetear su clave en {0}, por favor vaya al siguiente enlace:{3}{1}{3}{3}{2}</value>\n    <comment>{0}=title of forum, {1}=email link, {2}=signature, {3}=new line</comment>\n  </data>\n  <data name=\"ForgotPasswordSubject\" xml:space=\"preserve\">\n    <value>Instrucciones de reseteo de clave para {0}</value>\n    <comment>{0} is name of forum</comment>\n  </data>\n  <data name=\"ForumNoPost\" xml:space=\"preserve\">\n    <value>No tiene permisos para publicar en este foro.</value>\n  </data>\n  <data name=\"ForumNoView\" xml:space=\"preserve\">\n    <value>No tiene permisos para ver este foro</value>\n  </data>\n  <data name=\"LessThanMinute\" xml:space=\"preserve\">\n    <value>Hace menos de un minuto</value>\n  </data>\n  <data name=\"LoginToPost\" xml:space=\"preserve\">\n    <value>Debe haber ingresado para publicar.</value>\n  </data>\n  <data name=\"MinutesAgo\" xml:space=\"preserve\">\n    <value>Hace {0} minutos</value>\n    <comment>{0} is a number</comment>\n  </data>\n  <data name=\"More\" xml:space=\"preserve\">\n    <value>Mas</value>\n  </data>\n  <data name=\"NewPosts\" xml:space=\"preserve\">\n    <value>Nuevos mensajes</value>\n  </data>\n  <data name=\"NewPostsClosed\" xml:space=\"preserve\">\n    <value>Nuevos mensajes, tópico cerrado.</value>\n  </data>\n  <data name=\"NewPostsClosedPinned\" xml:space=\"preserve\">\n    <value>Nuevos Mensajes, tópicos cerrados y anclados.</value>\n  </data>\n  <data name=\"NewPostsPinned\" xml:space=\"preserve\">\n    <value>Nuevos mensajes, tópicos anclados.</value>\n  </data>\n  <data name=\"Next\" xml:space=\"preserve\">\n    <value>Siguiente</value>\n  </data>\n  <data name=\"NoNewPosts\" xml:space=\"preserve\">\n    <value>No hay nuevo mensajes</value>\n  </data>\n  <data name=\"NotLoggedIn\" xml:space=\"preserve\">\n    <value>No ha ingresado al sistema.</value>\n  </data>\n  <data name=\"OneMinuteAgo\" xml:space=\"preserve\">\n    <value>Hace 1 minuto</value>\n  </data>\n  <data name=\"Pinned\" xml:space=\"preserve\">\n    <value>Tópico anclado</value>\n  </data>\n  <data name=\"PMCreateWarnings\" xml:space=\"preserve\">\n\t  <value>Debe incluir mensaje y al menos un usuario.</value>\n  </data>\n  <data name=\"PostEmpty\" xml:space=\"preserve\">\n    <value>No se puede crear un mensaje vacio.</value>\n  </data>\n  <data name=\"PostWait\" xml:space=\"preserve\">\n    <value>Debe esperar {0} entre mensajes, y evita repetir el mensaje anterior.</value>\n    <comment>{0} is a number for seconds</comment>\n  </data>\n  <data name=\"Previous\" xml:space=\"preserve\">\n    <value>Anterior</value>\n  </data>\n  <data name=\"RegisterEmailSubject\" xml:space=\"preserve\">\n    <value>Registro en el foro - {0}</value>\n    <comment>{0} is name of forum</comment>\n  </data>\n  <data name=\"RegisterEmailThankYou\" xml:space=\"preserve\">\n    <value>Gracias por registrarse en {0}.{2}{2}{1}</value>\n    <comment>{0}=name of forum, {1}=forum link, {2}=new line</comment>\n  </data>\n  <data name=\"RegisterEmailThankYouVerify\" xml:space=\"preserve\">\n    <value>Gracias por registrarse en  {0}. Por favor siga este enlace para confirmar su registro:{5}{5}{1} {5}{5} Si no puede seguir el enlace, por favor visite la siguiente página, y copie y pegue el código de confirmación:{5}{5} Enlace: {2} {5}Código: {3} {5}{5}{4}</value>\n    <comment>{0}=name of forum, {1}=auth link, {2}=verify link, {3}=auth code, {4}=sig, {5}new line</comment>\n  </data>\n  <data name=\"SettingsSaved\" xml:space=\"preserve\">\n    <value>Configuracion grabada</value>\n  </data>\n  <data name=\"TodayTime\" xml:space=\"preserve\">\n    <value>Hoy, {0}</value>\n    <comment>{0} is a time like 1:43pm</comment>\n  </data>\n  <data name=\"TopicNotExist\" xml:space=\"preserve\">\n    <value>Ese tópico no existe</value>\n  </data>\n  <data name=\"YesterdayTime\" xml:space=\"preserve\">\n    <value>Ayer, {0}</value>\n    <comment>{0} a time like 1:43pm</comment>\n  </data>\n  <data name=\"Loading\" xml:space=\"preserve\">\n    <value>Cargando</value>\n  </data>\n  <data name=\"Voted\" xml:space=\"preserve\">\n    <value>Votado</value>\n  </data>\n  <data name=\"VoteUpPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; votó por un post en el tópico: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"ScoringGame\" xml:space=\"preserve\">\n    <value>Puntuación del juego</value>\n  </data>\n  <data name=\"Awards\" xml:space=\"preserve\">\n    <value>Premios</value>\n  </data>\n  <data name=\"EventDefinitions\" xml:space=\"preserve\">\n    <value>Definiciones caso</value>\n  </data>\n  <data name=\"AwardDefinitions\" xml:space=\"preserve\">\n    <value>Definiciones premio</value>\n  </data>\n  <data name=\"ManualEvent\" xml:space=\"preserve\">\n    <value>Manual de eventos</value>\n  </data>\n  <data name=\"None\" xml:space=\"preserve\">\n    <value>Nada</value>\n  </data>\n  <data name=\"ActivityFeed\" xml:space=\"preserve\">\n    <value>Feed de actividad</value>\n  </data>\n  <data name=\"NewReplyPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; hizo un post en el tema: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"NewPostPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; comenzó un nuevo tema: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to topic{3} is topic name</comment>\n  </data>\n  <data name=\"ExternalLogins\" xml:space=\"preserve\">\n    <value>Conexiones externas</value>\n  </data>\n  <data name=\"UseExistingForumAccount\" xml:space=\"preserve\">\n    <value>Usar cuenta del foro existente</value>\n  </data>\n  <data name=\"ExpiredLogin\" xml:space=\"preserve\">\n    <value>Esa conexión ha caducado o no es válido</value>\n  </data>\n  <data name=\"ExternalLoginsDisabled\" xml:space=\"preserve\">\n    <value>Logins externos están desactivadas</value>\n  </data>\n  <data name=\"NoExternalLoginsRegistered\" xml:space=\"preserve\">\n    <value>No tiene conexiones externas registrados</value>\n  </data>\n  <data name=\"DeletePermanently\" xml:space=\"preserve\">\n    <value>Eliminar permanentemente</value>\n  </data>\n  <data name=\"Preview\" xml:space=\"preserve\">\n    <value>Vista preliminar</value>\n  </data>\n  <data name=\"AskAQuestion\" xml:space=\"preserve\">\n    <value>Stellen Sie eine Frage</value>\n  </data>\n  <data name=\"Answers\" xml:space=\"preserve\">\n    <value>Respuestas</value>\n  </data>\n  <data name=\"ChooseAnswer\" xml:space=\"preserve\">\n    <value>Elija respuesta</value>\n  </data>\n  <data name=\"PostAnswer\" xml:space=\"preserve\">\n    <value>Publicar respuesta</value>\n  </data>\n  <data name=\"QuestionAnswered\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; optó por una respuesta para la pregunta: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"SubmitAnswer\" xml:space=\"preserve\">\n    <value>Enviar respuesta</value>\n  </data>\n  <data name=\"AppRestartRequired\" xml:space=\"preserve\">\n    <value>Aplicación requiere reiniciar</value>\n  </data>\n  <data name=\"SearchError\" xml:space=\"preserve\">\n    <value>Hubo un error de búsqueda</value>\n  </data>\n  <data name=\"CloseOldTopics\" xml:space=\"preserve\">\n    <value>Cerrar temas antiguos</value>\n  </data>\n  <data name=\"Days\" xml:space=\"preserve\">\n    <value>dias</value>\n  </data>\n  <data name=\"PrivateForum\" xml:space=\"preserve\">\n    <value>Foro privado</value>\n  </data>\n  <data name=\"AccountNotVerified\" xml:space=\"preserve\">\n    <value>Cuenta no verificada</value>\n  </data>\n  <data name=\"SelectText\" xml:space=\"preserve\">\n\t  <value>Elegir texto</value>\n  </data>\n  <data name=\"Notifications\" xml:space=\"preserve\">\n\t  <value>Notificaciones</value>\n  </data>\n  <data name=\"NewReplyNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; hizo un post en el tema: &lt;b&gt;{1}&lt;/b&gt;\n</value>\n  </data>\n  <data name=\"Award\" xml:space=\"preserve\">\n\t  <value>Premio</value>\n  </data>\n  <data name=\"VoteUpNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; votó por un post en el tópico: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"QuestionAnsweredNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; optó por una respuesta para la pregunta: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"AutoFollow\" xml:space=\"preserve\">\n\t  <value>Sigue automáticamente los temas a los que respondes</value>\n  </data>\n  <data name=\"MarkAllRead\" xml:space=\"preserve\">\n\t  <value>Marcar todo como leido</value>\n  </data>\n  <data name=\"UploadImage\" xml:space=\"preserve\">\n\t  <value>Cargar imagen</value>\n  </data>\n  <data name=\"Ignored\" xml:space=\"preserve\">\n\t  <value>Ignorada</value>\n  </data>\n  <data name=\"IgnoreList\" xml:space=\"preserve\">\n\t  <value>Lista de ignorados</value>\n  </data>\n  <data name=\"Ignore\" xml:space=\"preserve\">\n\t  <value>Ignorar</value>\n  </data>\n</root>"
  },
  {
    "path": "src/PopForums/Resources/Resources.fr.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"Account\" xml:space=\"preserve\">\n    <value>Compte</value>\n  </data>\n  <data name=\"AccountCreated\" xml:space=\"preserve\">\n    <value>Compte créé</value>\n  </data>\n  <data name=\"AccountReady\" xml:space=\"preserve\">\n    <value>Votre compte a été créé et vous pouvez commencer à utiliser le forum.</value>\n  </data>\n  <data name=\"AccountReadyCheckEmail\" xml:space=\"preserve\">\n    <value>Veuillez vérifier vos e-mails pour un lien de vérification afin de commencer à utiliser le forum.</value>\n  </data>\n  <data name=\"AccountVerified\" xml:space=\"preserve\">\n    <value>Votre compte a été vérifié</value>\n  </data>\n  <data name=\"AlreadyCreatedAccount\" xml:space=\"preserve\">\n    <value>Vous êtes déjà connecté et avez un compte.</value>\n  </data>\n  <data name=\"AsYouWouldLikeItToAppear\" xml:space=\"preserve\">\n    <value>(tel que vous souhaitez qu'il apparaisse)</value>\n  </data>\n  <data name=\"AtLeastSixChar\" xml:space=\"preserve\">\n    <value>(au moins six caractères)</value>\n  </data>\n  <data name=\"By\" xml:space=\"preserve\">\n    <value>par</value>\n    <comment>contexte : \"Dernier message : 12h par Jeff\"</comment>\n  </data>\n  <data name=\"CreateAccountButton\" xml:space=\"preserve\">\n    <value>Créer un compte</value>\n  </data>\n  <data name=\"CreateAnAccount\" xml:space=\"preserve\">\n    <value>Créer un compte</value>\n  </data>\n  <data name=\"EditYourProfile\" xml:space=\"preserve\">\n    <value>Modifier votre profil</value>\n  </data>\n  <data name=\"Email\" xml:space=\"preserve\">\n    <value>E-mail</value>\n  </data>\n  <data name=\"EmailProblemAccount\" xml:space=\"preserve\">\n    <value>Il y a eu un problème lors de l'envoi de votre e-mail de vérification : </value>\n  </data>\n  <data name=\"Favorites\" xml:space=\"preserve\">\n    <value>Favoris</value>\n  </data>\n  <data name=\"Forbidden\" xml:space=\"preserve\">\n    <value>Désolé, vous n'avez pas la permission de voir ceci.</value>\n  </data>\n  <data name=\"Forums\" xml:space=\"preserve\">\n    <value>Forums</value>\n  </data>\n  <data name=\"HaveReadTOS\" xml:space=\"preserve\">\n    <value>J'ai lu et j'accepte les conditions d'utilisation</value>\n  </data>\n  <data name=\"Last\" xml:space=\"preserve\">\n    <value>Dernier</value>\n  </data>\n  <data name=\"Login\" xml:space=\"preserve\">\n    <value>Connexion</value>\n  </data>\n  <data name=\"Logout\" xml:space=\"preserve\">\n    <value>Déconnexion</value>\n  </data>\n  <data name=\"MarkAllForumsRead\" xml:space=\"preserve\">\n    <value>Marquer tous les forums comme lus</value>\n  </data>\n  <data name=\"MyBirthdayIsOnOrBefore\" xml:space=\"preserve\">\n    <value>Mon anniversaire est le ou avant le {0}</value>\n    <comment>où {0} est une date</comment>\n  </data>\n  <data name=\"Name\" xml:space=\"preserve\">\n    <value>Nom</value>\n  </data>\n  <data name=\"NeedToVerifyExistingAccount\" xml:space=\"preserve\">\n    <value>Besoin de vérifier un compte existant ?</value>\n  </data>\n  <data name=\"PageNotFound\" xml:space=\"preserve\">\n    <value>Page non trouvée</value>\n  </data>\n  <data name=\"Password\" xml:space=\"preserve\">\n    <value>Mot de passe</value>\n  </data>\n  <data name=\"Posts\" xml:space=\"preserve\">\n    <value>Messages</value>\n  </data>\n  <data name=\"PrivateMessages\" xml:space=\"preserve\">\n    <value>Messages privés</value>\n  </data>\n  <data name=\"Recent\" xml:space=\"preserve\">\n    <value>Récent</value>\n  </data>\n  <data name=\"RegisteredUsers\" xml:space=\"preserve\">\n    <value>Utilisateurs inscrits</value>\n  </data>\n  <data name=\"RetypePassword\" xml:space=\"preserve\">\n    <value>Retapez le mot de passe</value>\n  </data>\n  <data name=\"Search\" xml:space=\"preserve\">\n    <value>Rechercher</value>\n  </data>\n  <data name=\"SubscribeToList\" xml:space=\"preserve\">\n    <value>S'abonner à la liste de diffusion</value>\n  </data>\n  <data name=\"Subscriptions\" xml:space=\"preserve\">\n    <value>Abonnements</value>\n  </data>\n  <data name=\"TermsOfService\" xml:space=\"preserve\">\n    <value>Conditions d'utilisation</value>\n  </data>\n  <data name=\"TimeZone\" xml:space=\"preserve\">\n    <value>Fuseau horaire</value>\n  </data>\n  <data name=\"Topics\" xml:space=\"preserve\">\n    <value>Sujets</value>\n  </data>\n  <data name=\"Total\" xml:space=\"preserve\">\n    <value>Total</value>\n  </data>\n  <data name=\"TotalPosts\" xml:space=\"preserve\">\n    <value>Total des messages</value>\n  </data>\n  <data name=\"TotalTopics\" xml:space=\"preserve\">\n    <value>Total des sujets</value>\n  </data>\n  <data name=\"UseDaylight\" xml:space=\"preserve\">\n    <value>Utiliser l'heure d'été Amérique du Nord/Europe</value>\n  </data>\n  <data name=\"UsersOnline\" xml:space=\"preserve\">\n    <value>Utilisateurs en ligne</value>\n  </data>\n  <data name=\"MustBe13\" xml:space=\"preserve\">\n    <value>Vous devez avoir 13 ans ou plus pour vous inscrire</value>\n  </data>\n  <data name=\"MustAcceptTOS\" xml:space=\"preserve\">\n    <value>Vous devez accepter les conditions d'utilisation</value>\n  </data>\n  <data name=\"RetypeYourPassword\" xml:space=\"preserve\">\n    <value>Retapez votre mot de passe</value>\n  </data>\n  <data name=\"NameRequired\" xml:space=\"preserve\">\n    <value>Nom requis</value>\n  </data>\n  <data name=\"NameInUse\" xml:space=\"preserve\">\n    <value>Ce nom est déjà utilisé</value>\n  </data>\n  <data name=\"EmailRequired\" xml:space=\"preserve\">\n    <value>E-mail requis</value>\n  </data>\n  <data name=\"ValidEmailAddressRequired\" xml:space=\"preserve\">\n    <value>Adresse e-mail valide requise</value>\n  </data>\n  <data name=\"EmailInUse\" xml:space=\"preserve\">\n    <value>Cette adresse e-mail est déjà utilisée</value>\n  </data>\n  <data name=\"EmailBanned\" xml:space=\"preserve\">\n    <value>Cette adresse e-mail a été bannie</value>\n  </data>\n  <data name=\"IPBanned\" xml:space=\"preserve\">\n    <value>Votre adresse IP a été bannie</value>\n  </data>\n  <data name=\"RetypePasswordMustMatch\" xml:space=\"preserve\">\n    <value>Le mot de passe retapé doit correspondre</value>\n  </data>\n  <data name=\"EditAccount\" xml:space=\"preserve\">\n    <value>Modifier le compte</value>\n  </data>\n  <data name=\"EditProfile\" xml:space=\"preserve\">\n    <value>Modifier le profil</value>\n  </data>\n  <data name=\"EnterVerificationCode\" xml:space=\"preserve\">\n    <value>Veuillez entrer votre code de vérification</value>\n  </data>\n  <data name=\"ForumHomePage\" xml:space=\"preserve\">\n    <value>Page d'accueil du forum</value>\n  </data>\n  <data name=\"MustBeRegisteredToEditAccount\" xml:space=\"preserve\">\n    <value>Vous devez être inscrit et connecté pour modifier votre compte.</value>\n  </data>\n  <data name=\"NoUserFoundWithEmail\" xml:space=\"preserve\">\n    <value>Aucun utilisateur trouvé avec cet e-mail</value>\n  </data>\n  <data name=\"SendEmailWithNewCodeButton\" xml:space=\"preserve\">\n    <value>Envoyer un e-mail avec un nouveau code</value>\n  </data>\n  <data name=\"VerificationEmailSent\" xml:space=\"preserve\">\n    <value>Votre e-mail de vérification a été envoyé</value>\n  </data>\n  <data name=\"VerificationFailure\" xml:space=\"preserve\">\n    <value>Échec de la vérification</value>\n  </data>\n  <data name=\"VerificationIfYouNeed\" xml:space=\"preserve\">\n    <value>Si vous avez besoin d'un nouveau code de vérification, entrez votre e-mail ici</value>\n  </data>\n  <data name=\"VerificationLinkBad\" xml:space=\"preserve\">\n    <value>Désolé, le lien de vérification que vous avez suivi ou le code que vous avez entré n'est pas valide.</value>\n  </data>\n  <data name=\"VerifyAccount\" xml:space=\"preserve\">\n    <value>Vérifier le compte</value>\n  </data>\n  <data name=\"VerifyCodeButton\" xml:space=\"preserve\">\n    <value>Vérifier le code</value>\n  </data>\n  <data name=\"Avatar\" xml:space=\"preserve\">\n    <value>Avatar</value>\n  </data>\n  <data name=\"AvatarDelete\" xml:space=\"preserve\">\n    <value>Supprimer cet avatar</value>\n  </data>\n  <data name=\"ChangeEmail\" xml:space=\"preserve\">\n    <value>Changer d'e-mail</value>\n  </data>\n  <data name=\"ChangeEmailButton\" xml:space=\"preserve\">\n    <value>Changer l'e-mail et envoyer le code</value>\n  </data>\n  <data name=\"ChangeEmailConsequence\" xml:space=\"preserve\">\n    <value>Changer votre adresse e-mail générera un nouveau code de vérification qui sera envoyé à la nouvelle adresse. Vous ne pourrez pas poster de messages tant que vous n'aurez pas vérifié votre nouvelle adresse e-mail avec le nouveau code.</value>\n  </data>\n  <data name=\"ChangePassword\" xml:space=\"preserve\">\n    <value>Changer le mot de passe</value>\n  </data>\n  <data name=\"ChangeYourEmailPassword\" xml:space=\"preserve\">\n    <value>Changer votre e-mail ou mot de passe</value>\n  </data>\n  <data name=\"DateOfBirth\" xml:space=\"preserve\">\n    <value>Date de naissance</value>\n  </data>\n  <data name=\"Details\" xml:space=\"preserve\">\n    <value>Détails</value>\n  </data>\n  <data name=\"EmailChangeSuccess\" xml:space=\"preserve\">\n    <value>Votre e-mail a été changé avec succès</value>\n  </data>\n  <data name=\"EmailNew\" xml:space=\"preserve\">\n    <value>Nouvelle adresse e-mail</value>\n  </data>\n  <data name=\"EmailNewRetype\" xml:space=\"preserve\">\n    <value>Retapez la nouvelle adresse e-mail</value>\n  </data>\n  <data name=\"EmailNotFound\" xml:space=\"preserve\">\n    <value>E-mail non trouvé</value>\n  </data>\n  <data name=\"EmailSent\" xml:space=\"preserve\">\n    <value>Votre e-mail a été envoyé</value>\n  </data>\n  <data name=\"EmailsMustMatch\" xml:space=\"preserve\">\n    <value>Ces adresses e-mail ne correspondent pas</value>\n  </data>\n  <data name=\"EmailUser\" xml:space=\"preserve\">\n    <value>Envoyer un e-mail à l'utilisateur</value>\n  </data>\n  <data name=\"ForcePlainTextBox\" xml:space=\"preserve\">\n    <value>Forcer les zones de texte brut</value>\n  </data>\n  <data name=\"ForgotInstructions\" xml:space=\"preserve\">\n    <value>Entrez l'adresse e-mail associée à votre compte pour recevoir les instructions de réinitialisation de votre mot de passe :</value>\n  </data>\n  <data name=\"ForgotInstructionsSent\" xml:space=\"preserve\">\n    <value>Instructions envoyées à votre e-mail</value>\n  </data>\n  <data name=\"ForgotPassword\" xml:space=\"preserve\">\n    <value>Mot de passe oublié</value>\n  </data>\n  <data name=\"ForgotPasswordQuestion\" xml:space=\"preserve\">\n    <value>Mot de passe oublié ?</value>\n  </data>\n  <data name=\"HideVanity\" xml:space=\"preserve\">\n    <value>Masquer la vanité</value>\n  </data>\n  <data name=\"In\" xml:space=\"preserve\">\n    <value>dans</value>\n    <comment>contexte : \"commencé par Jeff dans Forum Général\"</comment>\n  </data>\n  <data name=\"Joined\" xml:space=\"preserve\">\n    <value>Inscrit le</value>\n  </data>\n  <data name=\"Location\" xml:space=\"preserve\">\n    <value>Localisation</value>\n  </data>\n  <data name=\"LoginAlready\" xml:space=\"preserve\">\n    <value>Vous êtes déjà connecté.</value>\n  </data>\n  <data name=\"LoginBad\" xml:space=\"preserve\">\n    <value>E-mail ou mot de passe incorrect</value>\n  </data>\n  <data name=\"ManagePhotos\" xml:space=\"preserve\">\n    <value>Gérer les photos</value>\n  </data>\n  <data name=\"ManageYourPhotos\" xml:space=\"preserve\">\n    <value>Gérer vos photos</value>\n  </data>\n  <data name=\"NewPasswordSaved\" xml:space=\"preserve\">\n    <value>Nouveau mot de passe enregistré</value>\n  </data>\n  <data name=\"NotRegisteredQuestion\" xml:space=\"preserve\">\n    <value>Pas inscrit ?</value>\n  </data>\n  <data name=\"OldPasswordIncorrect\" xml:space=\"preserve\">\n    <value>Ancien mot de passe incorrect</value>\n  </data>\n  <data name=\"Options\" xml:space=\"preserve\">\n    <value>Options</value>\n  </data>\n  <data name=\"PasswordNew\" xml:space=\"preserve\">\n    <value>Nouveau mot de passe</value>\n  </data>\n  <data name=\"PasswordNewRetype\" xml:space=\"preserve\">\n    <value>Retapez le nouveau mot de passe</value>\n  </data>\n  <data name=\"PasswordOld\" xml:space=\"preserve\">\n    <value>Ancien mot de passe</value>\n  </data>\n  <data name=\"PasswordReset\" xml:space=\"preserve\">\n    <value>Réinitialiser le mot de passe</value>\n  </data>\n  <data name=\"PasswordResetLinkInvalid\" xml:space=\"preserve\">\n    <value>Ceci n'est pas un lien valide de réinitialisation de mot de passe.</value>\n  </data>\n  <data name=\"PasswordResetNote\" xml:space=\"preserve\">\n    <value>Votre nouveau mot de passe est actif et vous êtes maintenant connecté.</value>\n  </data>\n  <data name=\"PasswordResetSuccess\" xml:space=\"preserve\">\n    <value>Réinitialisation du mot de passe réussie</value>\n  </data>\n  <data name=\"Photo\" xml:space=\"preserve\">\n    <value>Photo</value>\n  </data>\n  <data name=\"PhotoDelete\" xml:space=\"preserve\">\n    <value>Supprimer cette photo</value>\n  </data>\n  <data name=\"PhotoNotApproved\" xml:space=\"preserve\">\n    <value>(cette image n'a pas encore été approuvée par un administrateur)</value>\n  </data>\n  <data name=\"ProfileUpdated\" xml:space=\"preserve\">\n    <value>Profil mis à jour</value>\n  </data>\n  <data name=\"RememberMe\" xml:space=\"preserve\">\n    <value>Se souvenir de moi ?</value>\n  </data>\n  <data name=\"Replies\" xml:space=\"preserve\">\n    <value>Réponses</value>\n  </data>\n  <data name=\"Save\" xml:space=\"preserve\">\n    <value>Enregistrer</value>\n  </data>\n  <data name=\"SendEmailButton\" xml:space=\"preserve\">\n    <value>Envoyer l'e-mail</value>\n  </data>\n  <data name=\"SendPM\" xml:space=\"preserve\">\n    <value>Envoyer un message privé</value>\n  </data>\n  <data name=\"ShowProfileDetails\" xml:space=\"preserve\">\n    <value>Afficher les détails de votre profil (sauf l'e-mail)</value>\n  </data>\n  <data name=\"Signature\" xml:space=\"preserve\">\n    <value>Signature</value>\n  </data>\n  <data name=\"StartedBy\" xml:space=\"preserve\">\n    <value>Commencé par</value>\n  </data>\n  <data name=\"Subject\" xml:space=\"preserve\">\n    <value>Sujet</value>\n  </data>\n  <data name=\"Text\" xml:space=\"preserve\">\n    <value>Texte</value>\n  </data>\n  <data name=\"To\" xml:space=\"preserve\">\n    <value>À</value>\n  </data>\n  <data name=\"UploadNew\" xml:space=\"preserve\">\n    <value>Téléverser nouveau</value>\n  </data>\n  <data name=\"UserNotFound\" xml:space=\"preserve\">\n    <value>Utilisateur non trouvé</value>\n  </data>\n  <data name=\"Views\" xml:space=\"preserve\">\n    <value>Vues</value>\n  </data>\n  <data name=\"Web\" xml:space=\"preserve\">\n    <value>Web</value>\n  </data>\n  <data name=\"WebVisit\" xml:space=\"preserve\">\n    <value>Visiter le site web de l'utilisateur</value>\n  </data>\n  <data name=\"YourIP\" xml:space=\"preserve\">\n    <value>Votre adresse IP</value>\n  </data>\n  <data name=\"Birthday\" xml:space=\"preserve\">\n    <value>Anniversaire</value>\n  </data>\n  <data name=\"Contact\" xml:space=\"preserve\">\n    <value>Contact</value>\n  </data>\n  <data name=\"Profile\" xml:space=\"preserve\">\n    <value>Profil</value>\n  </data>\n  <data name=\"SendNameEmail\" xml:space=\"preserve\">\n    <value>Envoyer un e-mail à {0}</value>\n    <comment>{0} est un nom</comment>\n  </data>\n  <data name=\"SendNamePM\" xml:space=\"preserve\">\n    <value>Envoyer un message privé à {0}</value>\n    <comment>{0} est un nom</comment>\n  </data>\n  <data name=\"Unsubscribe\" xml:space=\"preserve\">\n    <value>Se désabonner</value>\n  </data>\n  <data name=\"UnsubscribeFail\" xml:space=\"preserve\">\n    <value>Échec du désabonnement</value>\n  </data>\n  <data name=\"UnsubscribeLinkBad\" xml:space=\"preserve\">\n    <value>Le lien que vous avez suivi n'est pas valide.</value>\n  </data>\n  <data name=\"UnsubscribeNote\" xml:space=\"preserve\">\n    <value>Votre demande de désabonnement a été traitée. Vous pouvez vous réabonner en vous connectant et en allant sur votre page de compte.</value>\n  </data>\n  <data name=\"AddForum\" xml:space=\"preserve\">\n    <value>Ajouter un forum</value>\n  </data>\n  <data name=\"AddNew\" xml:space=\"preserve\">\n    <value>Ajouter nouveau</value>\n  </data>\n  <data name=\"AllowImages\" xml:space=\"preserve\">\n    <value>Autoriser les images</value>\n  </data>\n  <data name=\"Archived\" xml:space=\"preserve\">\n    <value>Archivé</value>\n  </data>\n  <data name=\"AreYouSure\" xml:space=\"preserve\">\n    <value>Êtes-vous sûr ?</value>\n  </data>\n  <data name=\"Ban\" xml:space=\"preserve\">\n    <value>Bannir</value>\n  </data>\n  <data name=\"BanRemove\" xml:space=\"preserve\">\n    <value>Retirer le bannissement</value>\n  </data>\n  <data name=\"Categories\" xml:space=\"preserve\">\n    <value>Catégories</value>\n  </data>\n  <data name=\"Category\" xml:space=\"preserve\">\n    <value>Catégorie</value>\n  </data>\n  <data name=\"CategoryEditTitle\" xml:space=\"preserve\">\n    <value>Modifier le titre de la catégorie</value>\n  </data>\n  <data name=\"CategoryMoveError\" xml:space=\"preserve\">\n    <value>Une erreur inconnue est survenue lors de la tentative de déplacement de la catégorie</value>\n  </data>\n  <data name=\"CensorReplacementChar\" xml:space=\"preserve\">\n    <value>Caractère de remplacement de censure</value>\n  </data>\n  <data name=\"CensorWords\" xml:space=\"preserve\">\n    <value>Mots à censurer</value>\n  </data>\n  <data name=\"Delete\" xml:space=\"preserve\">\n    <value>Supprimer</value>\n  </data>\n  <data name=\"DeleteAndBanButton\" xml:space=\"preserve\">\n    <value>Supprimer et bannir l'utilisateur</value>\n  </data>\n  <data name=\"DeleteUserButton\" xml:space=\"preserve\">\n    <value>Supprimer l'utilisateur</value>\n  </data>\n  <data name=\"Description\" xml:space=\"preserve\">\n    <value>Description</value>\n  </data>\n  <data name=\"Down\" xml:space=\"preserve\">\n    <value>Descendre</value>\n  </data>\n  <data name=\"Edit\" xml:space=\"preserve\">\n    <value>Modifier</value>\n  </data>\n  <data name=\"EditForum\" xml:space=\"preserve\">\n    <value>Modifier le forum</value>\n  </data>\n  <data name=\"EditUser\" xml:space=\"preserve\">\n    <value>Modifier l'utilisateur</value>\n  </data>\n  <data name=\"EmailBan\" xml:space=\"preserve\">\n    <value>Bannissement d'e-mail</value>\n  </data>\n  <data name=\"EmailIpBan\" xml:space=\"preserve\">\n    <value>Bannissement d'e-mail/IP</value>\n  </data>\n  <data name=\"EmailNewOptional\" xml:space=\"preserve\">\n    <value>Nouvel e-mail (optionnel)</value>\n  </data>\n  <data name=\"EmailUsers\" xml:space=\"preserve\">\n    <value>Envoyer un e-mail aux utilisateurs</value>\n  </data>\n  <data name=\"ErrorLog\" xml:space=\"preserve\">\n    <value>Journal des erreurs</value>\n  </data>\n  <data name=\"Forum\" xml:space=\"preserve\">\n    <value>Forum</value>\n  </data>\n  <data name=\"ForumAdapter\" xml:space=\"preserve\">\n    <value>Adaptateur de forum (optionnel, utiliser \"Namespace.Type, AssemblyName\")</value>\n  </data>\n  <data name=\"ForumHome\" xml:space=\"preserve\">\n    <value>Accueil du forum</value>\n  </data>\n  <data name=\"ForumPermisions\" xml:space=\"preserve\">\n    <value>Permissions du forum</value>\n  </data>\n  <data name=\"ForumSettings\" xml:space=\"preserve\">\n    <value>Paramètres du forum</value>\n  </data>\n  <data name=\"ForumsUncat\" xml:space=\"preserve\">\n    <value>Forums non catégorisés</value>\n  </data>\n  <data name=\"GeneralSettings\" xml:space=\"preserve\">\n    <value>Paramètres généraux</value>\n  </data>\n  <data name=\"IpBan\" xml:space=\"preserve\">\n    <value>Bannissement d'IP</value>\n  </data>\n  <data name=\"IpHistory\" xml:space=\"preserve\">\n    <value>Historique des IP</value>\n  </data>\n  <data name=\"IsApproved\" xml:space=\"preserve\">\n    <value>Est approuvé</value>\n  </data>\n  <data name=\"IsSubscribed\" xml:space=\"preserve\">\n    <value>Est abonné</value>\n  </data>\n  <data name=\"LogErrors\" xml:space=\"preserve\">\n    <value>Journaliser les erreurs</value>\n  </data>\n  <data name=\"Logging\" xml:space=\"preserve\">\n    <value>Journalisation</value>\n  </data>\n  <data name=\"LogMod\" xml:space=\"preserve\">\n    <value>Journaliser la modération</value>\n  </data>\n  <data name=\"LogSecurity\" xml:space=\"preserve\">\n    <value>Journaliser les actions de sécurité</value>\n  </data>\n  <data name=\"MinimumTimeBetweenPosts\" xml:space=\"preserve\">\n    <value>Temps minimum entre les messages (secondes)</value>\n  </data>\n  <data name=\"ModerationLog\" xml:space=\"preserve\">\n    <value>Journal de modération</value>\n  </data>\n  <data name=\"Move\" xml:space=\"preserve\">\n    <value>Déplacer</value>\n  </data>\n  <data name=\"NewUserApprovedWithoutVerification\" xml:space=\"preserve\">\n    <value>Le nouvel utilisateur est approuvé sans vérification</value>\n  </data>\n  <data name=\"NewUserImageApprovedWithoutMod\" xml:space=\"preserve\">\n    <value>L'image du nouvel utilisateur est approuvée sans modération</value>\n  </data>\n  <data name=\"Parsing\" xml:space=\"preserve\">\n    <value>Analyse</value>\n  </data>\n  <data name=\"PasswordNewOptional\" xml:space=\"preserve\">\n    <value>Nouveau mot de passe (optionnel)</value>\n  </data>\n  <data name=\"PopForumsAdmin\" xml:space=\"preserve\">\n    <value>Administration de POP Forums</value>\n  </data>\n  <data name=\"PostsPerPage\" xml:space=\"preserve\">\n    <value>Messages par page</value>\n  </data>\n  <data name=\"Role\" xml:space=\"preserve\">\n    <value>Rôle</value>\n  </data>\n  <data name=\"Roles\" xml:space=\"preserve\">\n    <value>Rôles</value>\n  </data>\n  <data name=\"Security\" xml:space=\"preserve\">\n    <value>Sécurité</value>\n  </data>\n  <data name=\"SecurityLog\" xml:space=\"preserve\">\n    <value>Journal de sécurité</value>\n  </data>\n  <data name=\"ServerTime\" xml:space=\"preserve\">\n    <value>Heure du serveur</value>\n  </data>\n  <data name=\"Services\" xml:space=\"preserve\">\n    <value>Services</value>\n  </data>\n  <data name=\"SessionLength\" xml:space=\"preserve\">\n    <value>Durée de session (minutes)</value>\n  </data>\n  <data name=\"Size\" xml:space=\"preserve\">\n    <value>Taille</value>\n  </data>\n  <data name=\"SubjectAndBodyNotEmpty\" xml:space=\"preserve\">\n    <value>Le sujet et le corps ne doivent pas être vides</value>\n  </data>\n  <data name=\"Title\" xml:space=\"preserve\">\n    <value>Titre</value>\n  </data>\n  <data name=\"TopicsPerPage\" xml:space=\"preserve\">\n    <value>Sujets par page</value>\n  </data>\n  <data name=\"Up\" xml:space=\"preserve\">\n    <value>Monter</value>\n  </data>\n  <data name=\"UserAvatarMaxDim\" xml:space=\"preserve\">\n    <value>Dimensions maximales de l'avatar utilisateur</value>\n  </data>\n  <data name=\"UserDeleteWarning\" xml:space=\"preserve\">\n    <value>ATTENTION ! Supprimer un compte est permanent</value>\n  </data>\n  <data name=\"UserImageApproval\" xml:space=\"preserve\">\n    <value>Approbation des images utilisateur</value>\n  </data>\n  <data name=\"UserImageMaxDim\" xml:space=\"preserve\">\n    <value>Dimensions maximales de l'image utilisateur</value>\n  </data>\n  <data name=\"UserRoles\" xml:space=\"preserve\">\n    <value>Rôles utilisateur</value>\n  </data>\n  <data name=\"Visible\" xml:space=\"preserve\">\n    <value>Visible</value>\n  </data>\n  <data name=\"AddNewForum\" xml:space=\"preserve\">\n    <value>Ajouter un nouveau forum</value>\n  </data>\n  <data name=\"Body\" xml:space=\"preserve\">\n    <value>Corps</value>\n  </data>\n  <data name=\"BodyHtml\" xml:space=\"preserve\">\n    <value>Corps HTML</value>\n  </data>\n  <data name=\"ClickToLoadMorePosts\" xml:space=\"preserve\">\n    <value>Cliquez pour charger des messages plus récents avant de répondre</value>\n  </data>\n  <data name=\"Comment\" xml:space=\"preserve\">\n    <value>Commentaire</value>\n  </data>\n  <data name=\"CommentsOptional\" xml:space=\"preserve\">\n    <value>Commentaires (optionnels)</value>\n  </data>\n  <data name=\"CreateJunkWordButton\" xml:space=\"preserve\">\n    <value>Créer un nouveau mot indésirable</value>\n  </data>\n  <data name=\"CreateNewReply\" xml:space=\"preserve\">\n    <value>Créez votre réponse</value>\n  </data>\n  <data name=\"CreateNewRole\" xml:space=\"preserve\">\n    <value>Créer un nouveau rôle</value>\n  </data>\n  <data name=\"CreateNewTopic\" xml:space=\"preserve\">\n    <value>Créez votre nouveau sujet</value>\n  </data>\n  <data name=\"DeleteAllErrors\" xml:space=\"preserve\">\n    <value>Supprimer toutes les erreurs</value>\n  </data>\n  <data name=\"DeleteJunkWordButton\" xml:space=\"preserve\">\n    <value>Supprimer le mot indésirable sélectionné</value>\n  </data>\n  <data name=\"DeleteSelectedRole\" xml:space=\"preserve\">\n    <value>Supprimer le rôle sélectionné</value>\n  </data>\n  <data name=\"EditPost\" xml:space=\"preserve\">\n    <value>Modifier le message</value>\n  </data>\n  <data name=\"EmailUsersQueued\" xml:space=\"preserve\">\n    <value>E-mail mis en file d'attente pour les utilisateurs abonnés.</value>\n  </data>\n  <data name=\"EndDate\" xml:space=\"preserve\">\n    <value>Date de fin</value>\n  </data>\n  <data name=\"Event\" xml:space=\"preserve\">\n    <value>Événement</value>\n  </data>\n  <data name=\"EventTime\" xml:space=\"preserve\">\n    <value>Heure de l'événement</value>\n  </data>\n  <data name=\"FavoriteMustBeLoggedIn\" xml:space=\"preserve\">\n    <value>Vous devez être connecté pour voir les sujets favoris.</value>\n  </data>\n  <data name=\"FavoritesDontHave\" xml:space=\"preserve\">\n    <value>Vous n'avez aucun sujet favori.</value>\n  </data>\n  <data name=\"FavoriteTopics\" xml:space=\"preserve\">\n    <value>Sujets favoris</value>\n  </data>\n  <data name=\"ForumMoveError\" xml:space=\"preserve\">\n    <value>Une erreur inconnue est survenue lors de la tentative de déplacement du forum</value>\n  </data>\n  <data name=\"ForumPermissionInstructions\" xml:space=\"preserve\">\n    <value>Pour supprimer les restrictions de publication ou de visualisation, supprimez tous les rôles de la case appropriée.</value>\n  </data>\n  <data name=\"FromEmailAddress\" xml:space=\"preserve\">\n    <value>Adresse e-mail de l'expéditeur</value>\n  </data>\n  <data name=\"ID\" xml:space=\"preserve\">\n    <value>ID</value>\n  </data>\n  <data name=\"IncludeSignature\" xml:space=\"preserve\">\n    <value>Inclure la signature</value>\n  </data>\n  <data name=\"IndexingInterval\" xml:space=\"preserve\">\n    <value>Intervalle d'indexation</value>\n  </data>\n  <data name=\"IP\" xml:space=\"preserve\">\n    <value>IP</value>\n  </data>\n  <data name=\"IsRunning\" xml:space=\"preserve\">\n    <value>Est en cours d'exécution</value>\n  </data>\n  <data name=\"JunkWords\" xml:space=\"preserve\">\n    <value>Mots indésirables</value>\n  </data>\n  <data name=\"MarkForumRead\" xml:space=\"preserve\">\n    <value>Marquer le forum comme lu</value>\n  </data>\n  <data name=\"Message\" xml:space=\"preserve\">\n    <value>Message</value>\n  </data>\n  <data name=\"Millseconds\" xml:space=\"preserve\">\n    <value>Millisecondes</value>\n  </data>\n  <data name=\"NamePosts\" xml:space=\"preserve\">\n    <value>Messages de {0}</value>\n    <comment>{0} est un nom</comment>\n  </data>\n  <data name=\"Optional\" xml:space=\"preserve\">\n    <value>(optionnel)</value>\n  </data>\n  <data name=\"PermRoles\" xml:space=\"preserve\">\n    <value>Admin et Modérateur sont des rôles permanents et ne peuvent pas être supprimés.</value>\n  </data>\n  <data name=\"PostID\" xml:space=\"preserve\">\n    <value>ID du message</value>\n  </data>\n  <data name=\"PostingRoles\" xml:space=\"preserve\">\n    <value>Rôles de publication</value>\n  </data>\n  <data name=\"PostNewTopic\" xml:space=\"preserve\">\n    <value>Publier un nouveau sujet</value>\n  </data>\n  <data name=\"PreviewTopic\" xml:space=\"preserve\">\n    <value>Aperçu du sujet</value>\n    <comment>prévisualiser le verbe, pas l'adjectif</comment>\n  </data>\n  <data name=\"Remove\" xml:space=\"preserve\">\n    <value>Retirer</value>\n  </data>\n  <data name=\"RemoveAll\" xml:space=\"preserve\">\n    <value>Tout retirer</value>\n  </data>\n  <data name=\"SendingInterval\" xml:space=\"preserve\">\n    <value>Intervalle d'envoi</value>\n  </data>\n  <data name=\"SmtpPassword\" xml:space=\"preserve\">\n    <value>Mot de passe SMTP</value>\n  </data>\n  <data name=\"SmtpPort\" xml:space=\"preserve\">\n    <value>Port (par défaut 25)</value>\n  </data>\n  <data name=\"SmtpServer\" xml:space=\"preserve\">\n    <value>Serveur SMTP</value>\n  </data>\n  <data name=\"SmtpUser\" xml:space=\"preserve\">\n    <value>Utilisateur SMTP</value>\n  </data>\n  <data name=\"StartDate\" xml:space=\"preserve\">\n    <value>Date de début</value>\n  </data>\n  <data name=\"SubmitNewTopic\" xml:space=\"preserve\">\n    <value>Soumettre un nouveau sujet</value>\n  </data>\n  <data name=\"SubmitReply\" xml:space=\"preserve\">\n    <value>Soumettre une réponse</value>\n  </data>\n  <data name=\"TopicID\" xml:space=\"preserve\">\n    <value>ID du sujet</value>\n  </data>\n  <data name=\"Type\" xml:space=\"preserve\">\n    <value>Type</value>\n  </data>\n  <data name=\"UseEsmtpCred\" xml:space=\"preserve\">\n    <value>Utiliser ESMTP pour les identifiants</value>\n  </data>\n  <data name=\"UserID\" xml:space=\"preserve\">\n    <value>ID utilisateur</value>\n  </data>\n  <data name=\"UserIDTarget\" xml:space=\"preserve\">\n    <value>ID utilisateur cible</value>\n  </data>\n  <data name=\"UserImageApprovalNotReq\" xml:space=\"preserve\">\n    <value>Les images téléversées par les utilisateurs ne nécessitent actuellement pas d'approbation</value>\n  </data>\n  <data name=\"UseSsl\" xml:space=\"preserve\">\n    <value>Utiliser SSL</value>\n  </data>\n  <data name=\"ViewingRoles\" xml:space=\"preserve\">\n    <value>Rôles de visualisation</value>\n  </data>\n  <data name=\"Close\" xml:space=\"preserve\">\n    <value>Fermer</value>\n  </data>\n  <data name=\"CloseOnReply\" xml:space=\"preserve\">\n    <value>Fermer à la réponse</value>\n  </data>\n  <data name=\"FavoriteMake\" xml:space=\"preserve\">\n    <value>Ajouter aux favoris</value>\n  </data>\n  <data name=\"FavoriteRemove\" xml:space=\"preserve\">\n    <value>Retirer des favoris</value>\n  </data>\n  <data name=\"Link\" xml:space=\"preserve\">\n    <value>Lien</value>\n  </data>\n  <data name=\"Moderator\" xml:space=\"preserve\">\n    <value>Modérateur</value>\n  </data>\n  <data name=\"NameAvatar\" xml:space=\"preserve\">\n    <value>Avatar de {0}</value>\n    <comment>{0} est un nom</comment>\n  </data>\n  <data name=\"NameLastEdit\" xml:space=\"preserve\">\n    <value>Dernière modification par {0}</value>\n    <comment>{0} est un nom</comment>\n  </data>\n  <data name=\"Open\" xml:space=\"preserve\">\n    <value>Ouvrir</value>\n  </data>\n  <data name=\"Pin\" xml:space=\"preserve\">\n    <value>Épingler</value>\n  </data>\n  <data name=\"PostReply\" xml:space=\"preserve\">\n    <value>Publier une réponse</value>\n  </data>\n  <data name=\"Quote\" xml:space=\"preserve\">\n    <value>Citer</value>\n  </data>\n  <data name=\"RecentTopics\" xml:space=\"preserve\">\n    <value>Sujets récents</value>\n  </data>\n  <data name=\"Reply\" xml:space=\"preserve\">\n    <value>Répondre</value>\n  </data>\n  <data name=\"ShowMorePosts\" xml:space=\"preserve\">\n    <value>Afficher plus de messages</value>\n  </data>\n  <data name=\"ShowPreviousPosts\" xml:space=\"preserve\">\n    <value>Afficher les messages précédents</value>\n  </data>\n  <data name=\"Subscribe\" xml:space=\"preserve\">\n    <value>S'abonner</value>\n  </data>\n  <data name=\"Undelete\" xml:space=\"preserve\">\n    <value>Restaurer</value>\n  </data>\n  <data name=\"Unpin\" xml:space=\"preserve\">\n    <value>Désépingler</value>\n  </data>\n  <data name=\"Update\" xml:space=\"preserve\">\n    <value>Mettre à jour</value>\n  </data>\n  <data name=\"Archive\" xml:space=\"preserve\">\n    <value>Archiver</value>\n  </data>\n  <data name=\"NewPM\" xml:space=\"preserve\">\n    <value>Nouveau message privé</value>\n  </data>\n  <data name=\"NoResults\" xml:space=\"preserve\">\n    <value>Aucun résultat trouvé</value>\n  </data>\n  <data name=\"Send\" xml:space=\"preserve\">\n    <value>Envoyer</value>\n  </data>\n  <data name=\"Unarchive\" xml:space=\"preserve\">\n    <value>Désarchiver</value>\n  </data>\n  <data name=\"ViewArchivedMessages\" xml:space=\"preserve\">\n    <value>Voir les messages archivés</value>\n  </data>\n  <data name=\"DisplayName\" xml:space=\"preserve\">\n    <value>Nom d'affichage</value>\n  </data>\n  <data name=\"Error\" xml:space=\"preserve\">\n    <value>Erreur</value>\n  </data>\n  <data name=\"ErrorSettingUpDb\" xml:space=\"preserve\">\n    <value>Une erreur est survenue lors de la configuration de votre base de données</value>\n  </data>\n  <data name=\"ForumReady\" xml:space=\"preserve\">\n    <value>Votre forum est prêt.</value>\n  </data>\n  <data name=\"ForumTitle\" xml:space=\"preserve\">\n    <value>Titre du forum</value>\n  </data>\n  <data name=\"GoAdmin\" xml:space=\"preserve\">\n    <value>Allez dans la section d'administration pour terminer la configuration de votre forum.</value>\n  </data>\n  <data name=\"InvalidLink\" xml:space=\"preserve\">\n    <value>Ce lien n'est plus valide.</value>\n  </data>\n  <data name=\"NoDataConnection\" xml:space=\"preserve\">\n    <value>Aucune connexion de données</value>\n  </data>\n  <data name=\"PopForumsSetup\" xml:space=\"preserve\">\n    <value>Configuration de POP Forums</value>\n  </data>\n  <data name=\"SetupCantConnect\" xml:space=\"preserve\">\n    <value>L'installation ne peut pas se connecter au magasin de données. Veuillez vérifier votre chaîne de connexion et votre configuration.</value>\n  </data>\n  <data name=\"SetupConnSuccess\" xml:space=\"preserve\">\n    <value>Une connexion à la base de données a été établie avec succès. Veuillez remplir les valeurs suivantes pour commencer</value>\n  </data>\n  <data name=\"SetupDatabase\" xml:space=\"preserve\">\n    <value>Configurer la base de données</value>\n  </data>\n  <data name=\"SetupFirstUser\" xml:space=\"preserve\">\n    <value>Configurez votre premier utilisateur (se verra accorder les droits d'administrateur et de modérateur)</value>\n  </data>\n  <data name=\"SubscribedTopics\" xml:space=\"preserve\">\n    <value>Sujets abonnés</value>\n  </data>\n  <data name=\"SubscribeLoggedIn\" xml:space=\"preserve\">\n    <value>Vous devez être connecté pour voir les sujets auxquels vous êtes abonné.</value>\n  </data>\n  <data name=\"SubscribeNone\" xml:space=\"preserve\">\n    <value>Vous n'êtes abonné à aucun sujet.</value>\n  </data>\n  <data name=\"Success\" xml:space=\"preserve\">\n    <value>Succès !</value>\n  </data>\n  <data name=\"UnsubscribeTopic\" xml:space=\"preserve\">\n    <value>{0} est désabonné de {1}</value>\n    <comment>{0} est un nom, {1} est un titre de sujet</comment>\n  </data>\n  <data name=\"ArchivedNewPosts\" xml:space=\"preserve\">\n    <value>Archivé avec de nouveaux messages</value>\n  </data>\n  <data name=\"Closed\" xml:space=\"preserve\">\n    <value>Sujet fermé</value>\n  </data>\n  <data name=\"ClosedPinned\" xml:space=\"preserve\">\n    <value>Sujet fermé, épinglé</value>\n  </data>\n  <data name=\"DeleteTopic\" xml:space=\"preserve\">\n    <value>Supprimer le sujet</value>\n  </data>\n  <data name=\"First\" xml:space=\"preserve\">\n    <value>Premier</value>\n  </data>\n  <data name=\"ForgotPasswordEmail\" xml:space=\"preserve\">\n    <value>Pour réinitialiser votre mot de passe sur {0}, veuillez suivre ce lien :{3}{1}{3}{3}{2}</value>\n    <comment>{0}=titre du forum, {1}=lien e-mail, {2}=signature, {3}=nouvelle ligne</comment>\n  </data>\n  <data name=\"ForgotPasswordSubject\" xml:space=\"preserve\">\n    <value>Vos instructions de réinitialisation de mot de passe pour {0}</value>\n    <comment>{0} est le nom du forum</comment>\n  </data>\n  <data name=\"ForumNoPost\" xml:space=\"preserve\">\n    <value>Vous n'êtes pas autorisé à publier dans ce forum</value>\n  </data>\n  <data name=\"ForumNoView\" xml:space=\"preserve\">\n    <value>Vous n'êtes pas autorisé à voir ce forum</value>\n  </data>\n  <data name=\"LessThanMinute\" xml:space=\"preserve\">\n    <value>Il y a moins d'une minute</value>\n  </data>\n  <data name=\"LoginToPost\" xml:space=\"preserve\">\n    <value>Vous devez être connecté pour publier</value>\n  </data>\n  <data name=\"MinutesAgo\" xml:space=\"preserve\">\n    <value>Il y a {0} minutes</value>\n    <comment>{0} est un nombre</comment>\n  </data>\n  <data name=\"More\" xml:space=\"preserve\">\n    <value>Plus</value>\n  </data>\n  <data name=\"NewPosts\" xml:space=\"preserve\">\n    <value>Nouveaux messages</value>\n  </data>\n  <data name=\"NewPostsClosed\" xml:space=\"preserve\">\n    <value>Nouveaux messages, sujet fermé</value>\n  </data>\n  <data name=\"NewPostsClosedPinned\" xml:space=\"preserve\">\n    <value>Nouveaux messages, sujet fermé &amp; épinglé</value>\n  </data>\n  <data name=\"NewPostsPinned\" xml:space=\"preserve\">\n    <value>Nouveaux messages, sujet épinglé</value>\n  </data>\n  <data name=\"Next\" xml:space=\"preserve\">\n    <value>Suivant</value>\n  </data>\n  <data name=\"NoNewPosts\" xml:space=\"preserve\">\n    <value>Aucun nouveau message</value>\n  </data>\n  <data name=\"NotLoggedIn\" xml:space=\"preserve\">\n    <value>Vous n'êtes pas connecté</value>\n  </data>\n  <data name=\"OneMinuteAgo\" xml:space=\"preserve\">\n    <value>Il y a 1 minute</value>\n  </data>\n  <data name=\"Pinned\" xml:space=\"preserve\">\n    <value>Sujet épinglé</value>\n  </data>\n  <data name=\"PMCreateWarnings\" xml:space=\"preserve\">\n\t  <value>Doit inclure un message et au moins un utilisateur</value>\n  </data>\n  <data name=\"PostEmpty\" xml:space=\"preserve\">\n    <value>Impossible de créer un message vide</value>\n  </data>\n  <data name=\"PostWait\" xml:space=\"preserve\">\n    <value>Vous devez attendre {0} secondes entre les messages et ne pouvez pas dupliquer votre dernier message</value>\n    <comment>{0} est un nombre de secondes</comment>\n  </data>\n  <data name=\"Previous\" xml:space=\"preserve\">\n    <value>Précédent</value>\n  </data>\n  <data name=\"RegisterEmailSubject\" xml:space=\"preserve\">\n    <value>Inscription au forum - {0}</value>\n    <comment>{0} est le nom du forum</comment>\n  </data>\n  <data name=\"RegisterEmailThankYou\" xml:space=\"preserve\">\n    <value>Merci de vous être inscrit sur {0}.{2}{2}{1}</value>\n    <comment>{0}=nom du forum, {1}=lien du forum, {2}=nouvelle ligne</comment>\n  </data>\n  <data name=\"RegisterEmailThankYouVerify\" xml:space=\"preserve\">\n    <value>Merci de vous être inscrit sur {0}. Veuillez suivre ce lien pour confirmer votre inscription :{5}{5}{1} {5}{5}Si vous ne pouvez pas suivre ce lien, veuillez visiter la page suivante et copier-coller le code de confirmation :{5}{5}Lien : {2} {5}Code : {3} {5}{5}{4}</value>\n    <comment>{0}=nom du forum, {1}=lien d'authentification, {2}=lien de vérification, {3}=code d'authentification, {4}=signature, {5}=nouvelle ligne</comment>\n  </data>\n  <data name=\"SettingsSaved\" xml:space=\"preserve\">\n    <value>Paramètres enregistrés</value>\n  </data>\n  <data name=\"TodayTime\" xml:space=\"preserve\">\n    <value>Aujourd'hui, {0}</value>\n    <comment>{0} est une heure comme 13h43</comment>\n  </data>\n  <data name=\"TopicNotExist\" xml:space=\"preserve\">\n    <value>Ce sujet n'existe pas</value>\n  </data>\n  <data name=\"YesterdayTime\" xml:space=\"preserve\">\n    <value>Hier, {0}</value>\n    <comment>{0} une heure comme 13h43</comment>\n  </data>\n  <data name=\"Loading\" xml:space=\"preserve\">\n    <value>Chargement</value>\n  </data>\n  <data name=\"Voted\" xml:space=\"preserve\">\n    <value>Voté</value>\n  </data>\n  <data name=\"VoteUpPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; a voté pour un message dans le sujet : &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} est l'URI de l'utilisateur, {1} est le nom d'utilisateur, {2} est l'URI du message, {3} est le nom du sujet</comment>\n  </data>\n  <data name=\"ScoringGame\" xml:space=\"preserve\">\n    <value>Jeu de score</value>\n  </data>\n  <data name=\"Awards\" xml:space=\"preserve\">\n    <value>Récompenses</value>\n  </data>\n  <data name=\"EventDefinitions\" xml:space=\"preserve\">\n    <value>Définitions d'événements</value>\n  </data>\n  <data name=\"AwardDefinitions\" xml:space=\"preserve\">\n    <value>Définitions de récompenses</value>\n  </data>\n  <data name=\"ManualEvent\" xml:space=\"preserve\">\n    <value>Événement manuel</value>\n  </data>\n  <data name=\"None\" xml:space=\"preserve\">\n    <value>Aucun</value>\n  </data>\n  <data name=\"ActivityFeed\" xml:space=\"preserve\">\n    <value>Flux d'activité</value>\n  </data>\n  <data name=\"NewReplyPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; a fait un message dans le sujet : &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} est l'URI de l'utilisateur, {1} est le nom d'utilisateur, {2} est l'URI du message, {3} est le nom du sujet</comment>\n  </data>\n  <data name=\"NewPostPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; a commencé un nouveau sujet : &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} est l'URI de l'utilisateur, {1} est le nom d'utilisateur, {2} est l'URI du sujet, {3} est le nom du sujet</comment>\n  </data>\n  <data name=\"ExternalLogins\" xml:space=\"preserve\">\n    <value>Connexions externes</value>\n  </data>\n  <data name=\"UseExistingForumAccount\" xml:space=\"preserve\">\n    <value>Utiliser un compte forum existant</value>\n  </data>\n  <data name=\"ExpiredLogin\" xml:space=\"preserve\">\n    <value>Cette connexion a expiré ou n'est pas valide</value>\n  </data>\n  <data name=\"ExternalLoginsDisabled\" xml:space=\"preserve\">\n    <value>Les connexions externes sont désactivées</value>\n  </data>\n  <data name=\"NoExternalLoginsRegistered\" xml:space=\"preserve\">\n    <value>Vous n'avez aucune connexion externe enregistrée</value>\n  </data>\n  <data name=\"DeletePermanently\" xml:space=\"preserve\">\n    <value>Supprimer définitivement</value>\n  </data>\n  <data name=\"Preview\" xml:space=\"preserve\">\n    <value>Aperçu</value>\n  </data>\n  <data name=\"AskAQuestion\" xml:space=\"preserve\">\n    <value>Poser une question</value>\n  </data>\n  <data name=\"Answers\" xml:space=\"preserve\">\n    <value>Réponses</value>\n  </data>\n  <data name=\"QuestionAnswered\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; a choisi une réponse pour la question : &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} est l'URI de l'utilisateur, {1} est le nom d'utilisateur, {2} est l'URI du message, {3} est le nom du sujet</comment>\n  </data>\n  <data name=\"ChooseAnswer\" xml:space=\"preserve\">\n    <value>Choisir une réponse</value>\n  </data>\n  <data name=\"PostAnswer\" xml:space=\"preserve\">\n    <value>Poster une réponse</value>\n  </data>\n  <data name=\"SubmitAnswer\" xml:space=\"preserve\">\n    <value>Soumettre une réponse</value>\n  </data>\n  <data name=\"AppRestartRequired\" xml:space=\"preserve\">\n    <value>Un redémarrage de l'application est requis</value>\n  </data>\n  <data name=\"SearchError\" xml:space=\"preserve\">\n    <value>Une erreur de recherche est survenue</value>\n  </data>\n  <data name=\"BotError\" xml:space=\"preserve\">\n    <value>Vous pourriez être un robot</value>\n  </data>\n  <data name=\"CloseOldTopics\" xml:space=\"preserve\">\n    <value>Fermer les anciens sujets</value>\n  </data>\n  <data name=\"Days\" xml:space=\"preserve\">\n    <value>jours</value>\n  </data>\n  <data name=\"PrivateForum\" xml:space=\"preserve\">\n    <value>Forum privé</value>\n  </data>\n  <data name=\"AccountNotVerified\" xml:space=\"preserve\">\n    <value>Le compte n'est pas vérifié</value>\n  </data>\n  <data name=\"SelectText\" xml:space=\"preserve\">\n\t  <value>Sélectionner le texte</value>\n  </data>\n  <data name=\"Notifications\" xml:space=\"preserve\">\n\t  <value>Notifications</value>\n  </data>\n  <data name=\"NewReplyNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; a fait un message dans le sujet : &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"Award\" xml:space=\"preserve\">\n\t  <value>Récompense</value>\n  </data>\n  <data name=\"VoteUpNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; a voté pour un message dans le sujet : &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"QuestionAnsweredNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; a choisi une réponse pour la question : &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"AutoFollow\" xml:space=\"preserve\">\n\t  <value>Suivre automatiquement les sujets auxquels vous répondez</value>\n  </data>\n  <data name=\"MarkAllRead\" xml:space=\"preserve\">\n\t  <value>Tout marquer comme lu</value>\n  </data>\n  <data name=\"UploadImage\" xml:space=\"preserve\">\n\t  <value>Téléverser une image</value>\n  </data>\n  <data name=\"Ignored\" xml:space=\"preserve\">\n\t  <value>Ignoré</value>\n  </data>\n  <data name=\"Ignore\" xml:space=\"preserve\">\n\t  <value>Ignorer</value>\n  </data>\n  <data name=\"IgnoreList\" xml:space=\"preserve\">\n\t  <value>Liste d'ignorés</value>\n  </data>\n</root>"
  },
  {
    "path": "src/PopForums/Resources/Resources.nl.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"Account\" xml:space=\"preserve\">\n    <value>Account</value>\n  </data>\n  <data name=\"AccountCreated\" xml:space=\"preserve\">\n    <value>Account Aangemaakt</value>\n  </data>\n  <data name=\"AccountReady\" xml:space=\"preserve\">\n    <value>Je account is aangemaakt en je kunt nu gebruik maken van het forum.</value>\n  </data>\n  <data name=\"AccountReadyCheckEmail\" xml:space=\"preserve\">\n    <value>Controleer a.u.b. je e-mail voor de verificatielink om gebruik te kunnen maken van het forum.</value>\n  </data>\n  <data name=\"AccountVerified\" xml:space=\"preserve\">\n    <value>Je account is geverifieerd</value>\n  </data>\n  <data name=\"AlreadyCreatedAccount\" xml:space=\"preserve\">\n    <value>Je bent reeds ingelogd en hebt een account.</value>\n  </data>\n  <data name=\"AsYouWouldLikeItToAppear\" xml:space=\"preserve\">\n    <value>(zoals je het eruit wilt laten zien)</value>\n  </data>\n  <data name=\"AtLeastSixChar\" xml:space=\"preserve\">\n    <value>(op z'n minst zes karakters)</value>\n  </data>\n  <data name=\"By\" xml:space=\"preserve\">\n    <value>by</value>\n    <comment>context: \"Laatste post: 12am door Jeff\"</comment>\n  </data>\n  <data name=\"CreateAccountButton\" xml:space=\"preserve\">\n    <value>Maak Account</value>\n  </data>\n  <data name=\"CreateAnAccount\" xml:space=\"preserve\">\n    <value>Maak een account</value>\n  </data>\n  <data name=\"EditYourProfile\" xml:space=\"preserve\">\n    <value>Bewerk je profiel</value>\n  </data>\n  <data name=\"Email\" xml:space=\"preserve\">\n    <value>E-mail</value>\n  </data>\n  <data name=\"EmailProblemAccount\" xml:space=\"preserve\">\n    <value>Er was een probleem bij het verzenden van je verificatiemail: </value>\n  </data>\n  <data name=\"Favorites\" xml:space=\"preserve\">\n    <value>Favorieten</value>\n  </data>\n  <data name=\"Forbidden\" xml:space=\"preserve\">\n    <value>Sorry, je hebt niet de juiste bevoegdheden om dit te bekijken.</value>\n  </data>\n  <data name=\"Forums\" xml:space=\"preserve\">\n    <value>Forums</value>\n  </data>\n  <data name=\"HaveReadTOS\" xml:space=\"preserve\">\n    <value>Ik heb de algemene voorwaarden gelezen en ga hiermee akkoord</value>\n  </data>\n  <data name=\"Last\" xml:space=\"preserve\">\n    <value>Laatste</value>\n  </data>\n  <data name=\"Login\" xml:space=\"preserve\">\n    <value>Login</value>\n  </data>\n  <data name=\"Logout\" xml:space=\"preserve\">\n    <value>Logout</value>\n  </data>\n  <data name=\"MarkAllForumsRead\" xml:space=\"preserve\">\n    <value>Markeer alle forums als gelezen</value>\n  </data>\n  <data name=\"MyBirthdayIsOnOrBefore\" xml:space=\"preserve\">\n    <value>Mijn geboortedatum is voor {0}</value>\n    <comment>where {0} is a date</comment>\n  </data>\n  <data name=\"Name\" xml:space=\"preserve\">\n    <value>Naam</value>\n  </data>\n  <data name=\"NeedToVerifyExistingAccount\" xml:space=\"preserve\">\n    <value>Nodig om een bestaand account te verifieren?</value>\n  </data>\n  <data name=\"PageNotFound\" xml:space=\"preserve\">\n    <value>Pagina niet gevonden</value>\n  </data>\n  <data name=\"Password\" xml:space=\"preserve\">\n    <value>Wachtwoord</value>\n  </data>\n  <data name=\"Posts\" xml:space=\"preserve\">\n    <value>Posts</value>\n  </data>\n  <data name=\"PrivateMessages\" xml:space=\"preserve\">\n    <value>Privé Berichten</value>\n  </data>\n  <data name=\"Recent\" xml:space=\"preserve\">\n    <value>Recent</value>\n  </data>\n  <data name=\"RegisteredUsers\" xml:space=\"preserve\">\n    <value>Geregistreerde gebruikers</value>\n  </data>\n  <data name=\"RetypePassword\" xml:space=\"preserve\">\n    <value>Type wachtwoord opnieuw</value>\n  </data>\n  <data name=\"Search\" xml:space=\"preserve\">\n    <value>Zoek</value>\n  </data>\n  <data name=\"SubscribeToList\" xml:space=\"preserve\">\n    <value>Aanmelden voor de mailing list</value>\n  </data>\n  <data name=\"Subscriptions\" xml:space=\"preserve\">\n    <value>Aanmeldingen</value>\n  </data>\n  <data name=\"TermsOfService\" xml:space=\"preserve\">\n    <value>Algemene Voorwaarden</value>\n  </data>\n  <data name=\"TimeZone\" xml:space=\"preserve\">\n    <value>Tijdzone</value>\n  </data>\n  <data name=\"Topics\" xml:space=\"preserve\">\n    <value>Topics</value>\n  </data>\n  <data name=\"Total\" xml:space=\"preserve\">\n    <value>Totaal</value>\n  </data>\n  <data name=\"TotalPosts\" xml:space=\"preserve\">\n    <value>Totaal aantal posts</value>\n  </data>\n  <data name=\"TotalTopics\" xml:space=\"preserve\">\n    <value>Totaal aantal topics</value>\n  </data>\n  <data name=\"UseDaylight\" xml:space=\"preserve\">\n    <value>Use N.America/Europe Daylight Saving</value>\n  </data>\n  <data name=\"UsersOnline\" xml:space=\"preserve\">\n    <value>Gebruikers Online</value>\n  </data>\n  <data name=\"MustBe13\" xml:space=\"preserve\">\n    <value>Je moet 13 of ouder zijn om je te kunnen registreren</value>\n  </data>\n  <data name=\"MustAcceptTOS\" xml:space=\"preserve\">\n    <value>Je moet de algemene voorwaarden accepteren</value>\n  </data>\n  <data name=\"RetypeYourPassword\" xml:space=\"preserve\">\n    <value>Voer je wachtwoord opnieuw in</value>\n  </data>\n  <data name=\"NameRequired\" xml:space=\"preserve\">\n    <value>Naam verplicht</value>\n  </data>\n  <data name=\"NameInUse\" xml:space=\"preserve\">\n    <value>Die naam is reeds in gebruik</value>\n  </data>\n  <data name=\"EmailRequired\" xml:space=\"preserve\">\n    <value>E-mail is verplicht</value>\n  </data>\n  <data name=\"ValidEmailAddressRequired\" xml:space=\"preserve\">\n    <value>Geldig e-mail adres is verplicht</value>\n  </data>\n  <data name=\"EmailInUse\" xml:space=\"preserve\">\n    <value>Dat e-mail adres wordt al gebruikt</value>\n  </data>\n  <data name=\"EmailBanned\" xml:space=\"preserve\">\n    <value>Dat e-mail adres is geblokkeerd</value>\n  </data>\n  <data name=\"IPBanned\" xml:space=\"preserve\">\n    <value>Je IP adres is geblokkeerd</value>\n  </data>\n  <data name=\"RetypePasswordMustMatch\" xml:space=\"preserve\">\n    <value>Het opnieuw ingevoerde wachtwoord moet overeenkomen</value>\n  </data>\n  <data name=\"EditAccount\" xml:space=\"preserve\">\n    <value>Bewerk Account</value>\n  </data>\n  <data name=\"EditProfile\" xml:space=\"preserve\">\n    <value>Bewerk Profiel</value>\n  </data>\n  <data name=\"EnterVerificationCode\" xml:space=\"preserve\">\n    <value>Voer a.u.b. je verificatiecode in</value>\n  </data>\n  <data name=\"ForumHomePage\" xml:space=\"preserve\">\n    <value>Forum home page</value>\n  </data>\n  <data name=\"MustBeRegisteredToEditAccount\" xml:space=\"preserve\">\n    <value>Je moet geregistreerd en ingelogd zijn om je account te kunnen editen.</value>\n  </data>\n  <data name=\"NoUserFoundWithEmail\" xml:space=\"preserve\">\n    <value>Er is geen gebruiker gevonden met dat e-mail adres</value>\n  </data>\n  <data name=\"SendEmailWithNewCodeButton\" xml:space=\"preserve\">\n    <value>Stuur E-mail Met Nieuwe Code</value>\n  </data>\n  <data name=\"VerificationEmailSent\" xml:space=\"preserve\">\n    <value>Je verificatiemail is verzonden</value>\n  </data>\n  <data name=\"VerificationFailure\" xml:space=\"preserve\">\n    <value>Verificatie Mislukt</value>\n  </data>\n  <data name=\"VerificationIfYouNeed\" xml:space=\"preserve\">\n    <value>Als je een nieuwe verificatiecode nodig hebt, voer dan hier je e-mailadres in</value>\n  </data>\n  <data name=\"VerificationLinkBad\" xml:space=\"preserve\">\n    <value>Sorry, de verificatielink waarop je hebt geklikt of de code die je ingevoerd hebt is ongeldig.</value>\n  </data>\n  <data name=\"VerifyAccount\" xml:space=\"preserve\">\n    <value>Controleer Account</value>\n  </data>\n  <data name=\"VerifyCodeButton\" xml:space=\"preserve\">\n    <value>Controleer Code</value>\n  </data>\n  <data name=\"Avatar\" xml:space=\"preserve\">\n    <value>Avatar</value>\n  </data>\n  <data name=\"AvatarDelete\" xml:space=\"preserve\">\n    <value>Verwijder deze avatar</value>\n  </data>\n  <data name=\"ChangeEmail\" xml:space=\"preserve\">\n    <value>Verander E-mailadres</value>\n  </data>\n  <data name=\"ChangeEmailButton\" xml:space=\"preserve\">\n    <value>Verander e-mailadres en verzend code</value>\n  </data>\n  <data name=\"ChangeEmailConsequence\" xml:space=\"preserve\">\n    <value>Bij het veranderen van je e-mailadres, zal er een nieuwe verificatiecode gegenereerd en verstuurd worden naar het nieuwe e-mailadres. Je zult niet meer kunnen posten totdat je e-mailadres is geverifieerd met de nieuwe code.</value>\n  </data>\n  <data name=\"ChangePassword\" xml:space=\"preserve\">\n    <value>Verander Wachtwoord</value>\n  </data>\n  <data name=\"ChangeYourEmailPassword\" xml:space=\"preserve\">\n    <value>Verander je e-mailadres of wachtwoord</value>\n  </data>\n  <data name=\"DateOfBirth\" xml:space=\"preserve\">\n    <value>Geboortedatum</value>\n  </data>\n  <data name=\"Details\" xml:space=\"preserve\">\n    <value>Details</value>\n  </data>\n  <data name=\"EmailChangeSuccess\" xml:space=\"preserve\">\n    <value>Je e-mailadres is succesvol aangepast</value>\n  </data>\n  <data name=\"EmailNew\" xml:space=\"preserve\">\n    <value>Nieuw e-mailadres</value>\n  </data>\n  <data name=\"EmailNewRetype\" xml:space=\"preserve\">\n    <value>Voer het nieuwe e-mailadres nogmaals in</value>\n  </data>\n  <data name=\"EmailNotFound\" xml:space=\"preserve\">\n    <value>E-mailadres niet gevonden</value>\n  </data>\n  <data name=\"EmailSent\" xml:space=\"preserve\">\n    <value>Je e-mail is verzonden</value>\n  </data>\n  <data name=\"EmailsMustMatch\" xml:space=\"preserve\">\n    <value>De e-mailadressen komen niet overeen</value>\n  </data>\n  <data name=\"EmailUser\" xml:space=\"preserve\">\n    <value>Mail Gebruiker</value>\n  </data>\n  <data name=\"ForcePlainTextBox\" xml:space=\"preserve\">\n    <value>Forceer uitsluitend-tekst tekstvelden</value>\n  </data>\n  <data name=\"ForgotInstructions\" xml:space=\"preserve\">\n    <value>Voer het e-mailadres in dat geassocieerd is met je account om de instructies voor het resetten van je wachtwoord te ontvangen:</value>\n  </data>\n  <data name=\"ForgotInstructionsSent\" xml:space=\"preserve\">\n    <value>Instructies zijn verzonden naar je e-mailadres</value>\n  </data>\n  <data name=\"ForgotPassword\" xml:space=\"preserve\">\n    <value>Wachtwoord Vergeten</value>\n  </data>\n  <data name=\"ForgotPasswordQuestion\" xml:space=\"preserve\">\n    <value>Je wachtwoord vergeten?</value>\n  </data>\n  <data name=\"HideVanity\" xml:space=\"preserve\">\n    <value>Verberg ijdelheid</value>\n  </data>\n  <data name=\"In\" xml:space=\"preserve\">\n    <value>in</value>\n    <comment>context: gestart door Jeff in General Forum</comment>\n  </data>\n  <data name=\"Joined\" xml:space=\"preserve\">\n    <value>Samengekomen</value>\n  </data>\n  <data name=\"Location\" xml:space=\"preserve\">\n    <value>Lokatie</value>\n  </data>\n  <data name=\"LoginAlready\" xml:space=\"preserve\">\n    <value>Je was al ingelogd.</value>\n  </data>\n  <data name=\"LoginBad\" xml:space=\"preserve\">\n    <value>Verkeerd e-mailadres of wachtwoord</value>\n  </data>\n  <data name=\"ManagePhotos\" xml:space=\"preserve\">\n    <value>Beheer Foto's</value>\n  </data>\n  <data name=\"ManageYourPhotos\" xml:space=\"preserve\">\n    <value>Beheer je foto's</value>\n  </data>\n  <data name=\"NewPasswordSaved\" xml:space=\"preserve\">\n    <value>Nieuw wachtwoord opgeslagen</value>\n  </data>\n  <data name=\"NotRegisteredQuestion\" xml:space=\"preserve\">\n    <value>Niet geregisteerd?</value>\n  </data>\n  <data name=\"OldPasswordIncorrect\" xml:space=\"preserve\">\n    <value>Het oude wachtwoord is incorrect</value>\n  </data>\n  <data name=\"Options\" xml:space=\"preserve\">\n    <value>Opties</value>\n  </data>\n  <data name=\"PasswordNew\" xml:space=\"preserve\">\n    <value>Nieuw wachtwoord</value>\n  </data>\n  <data name=\"PasswordNewRetype\" xml:space=\"preserve\">\n    <value>Nieuw wachtwoord nogmaals</value>\n  </data>\n  <data name=\"PasswordOld\" xml:space=\"preserve\">\n    <value>Oud wachtwoord</value>\n  </data>\n  <data name=\"PasswordReset\" xml:space=\"preserve\">\n    <value>Reset Wachtwoord</value>\n  </data>\n  <data name=\"PasswordResetLinkInvalid\" xml:space=\"preserve\">\n    <value>Dit is geen geldige wachtwoord reset link.</value>\n  </data>\n  <data name=\"PasswordResetNote\" xml:space=\"preserve\">\n    <value>Je nieuwe wachtwoord is  actief en je bent nu ingelogd.</value>\n  </data>\n  <data name=\"PasswordResetSuccess\" xml:space=\"preserve\">\n    <value>Wachtwoord Reset Succesvol</value>\n  </data>\n  <data name=\"Photo\" xml:space=\"preserve\">\n    <value>Foto</value>\n  </data>\n  <data name=\"PhotoDelete\" xml:space=\"preserve\">\n    <value>Verwijder deze foto</value>\n  </data>\n  <data name=\"PhotoNotApproved\" xml:space=\"preserve\">\n    <value>(dit plaatje is nog niet goedgekeurd door een beheerder)</value>\n  </data>\n  <data name=\"ProfileUpdated\" xml:space=\"preserve\">\n    <value>Profiel bijgewerkt</value>\n  </data>\n  <data name=\"RememberMe\" xml:space=\"preserve\">\n    <value>Onthouden?</value>\n  </data>\n  <data name=\"Replies\" xml:space=\"preserve\">\n    <value>Antwoorden</value>\n  </data>\n  <data name=\"Save\" xml:space=\"preserve\">\n    <value>Opslaan</value>\n  </data>\n  <data name=\"SendEmailButton\" xml:space=\"preserve\">\n    <value>Verstuur Mail</value>\n  </data>\n  <data name=\"SendPM\" xml:space=\"preserve\">\n    <value>Verstuur privébericht</value>\n  </data>\n  <data name=\"ShowProfileDetails\" xml:space=\"preserve\">\n    <value>Toon je profieldetails (met geen e-mail)</value>\n  </data>\n  <data name=\"Signature\" xml:space=\"preserve\">\n    <value>Handtekening</value>\n  </data>\n  <data name=\"StartedBy\" xml:space=\"preserve\">\n    <value>Gestart door</value>\n  </data>\n  <data name=\"Subject\" xml:space=\"preserve\">\n    <value>Onderwerk</value>\n  </data>\n  <data name=\"Text\" xml:space=\"preserve\">\n    <value>Tekst</value>\n  </data>\n  <data name=\"To\" xml:space=\"preserve\">\n    <value>Aan</value>\n  </data>\n  <data name=\"UploadNew\" xml:space=\"preserve\">\n    <value>Upload nieuw</value>\n  </data>\n  <data name=\"UserNotFound\" xml:space=\"preserve\">\n    <value>Gebruiker niet gevonden</value>\n  </data>\n  <data name=\"Views\" xml:space=\"preserve\">\n    <value>Weergaven</value>\n  </data>\n  <data name=\"Web\" xml:space=\"preserve\">\n    <value>Web</value>\n  </data>\n  <data name=\"WebVisit\" xml:space=\"preserve\">\n    <value>Bezoek gebruiker web site</value>\n  </data>\n  <data name=\"YourIP\" xml:space=\"preserve\">\n    <value>Jou IP adres</value>\n  </data>\n  <data name=\"Birthday\" xml:space=\"preserve\">\n    <value>Geboortedatum</value>\n  </data>\n  <data name=\"Contact\" xml:space=\"preserve\">\n    <value>Contact</value>\n  </data>\n  <data name=\"Profile\" xml:space=\"preserve\">\n    <value>Profiel</value>\n  </data>\n  <data name=\"SendNameEmail\" xml:space=\"preserve\">\n    <value>Verstuur {0} een e-mail bericht</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"SendNamePM\" xml:space=\"preserve\">\n    <value>Verstuur {0} een privébericht</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Unsubscribe\" xml:space=\"preserve\">\n    <value>Afmelden</value>\n  </data>\n  <data name=\"UnsubscribeFail\" xml:space=\"preserve\">\n    <value>Fout bij Afmelden</value>\n  </data>\n  <data name=\"UnsubscribeLinkBad\" xml:space=\"preserve\">\n    <value>De link waarop je klikte is ongeldig.</value>\n  </data>\n  <data name=\"UnsubscribeNote\" xml:space=\"preserve\">\n    <value>Je afmeldingsaanvraag is verwerkt. Je kunt je weer aanmelden door in te loggen en naar je accountpagine te gaan.</value>\n  </data>\n  <data name=\"AddForum\" xml:space=\"preserve\">\n    <value>Forum Toevoegen</value>\n  </data>\n  <data name=\"AddNew\" xml:space=\"preserve\">\n    <value>Nieuwe Toevoegen</value>\n  </data>\n  <data name=\"AllowImages\" xml:space=\"preserve\">\n    <value>Afbeeldingen toestaan</value>\n  </data>\n  <data name=\"Archived\" xml:space=\"preserve\">\n    <value>Gearchiveerd</value>\n  </data>\n  <data name=\"AreYouSure\" xml:space=\"preserve\">\n    <value>Weet je het zeker?</value>\n  </data>\n  <data name=\"Ban\" xml:space=\"preserve\">\n    <value>Blokkeren</value>\n  </data>\n  <data name=\"BanRemove\" xml:space=\"preserve\">\n    <value>Verwijder Blokkade</value>\n  </data>\n  <data name=\"Categories\" xml:space=\"preserve\">\n    <value>Categorieën</value>\n  </data>\n  <data name=\"Category\" xml:space=\"preserve\">\n    <value>Categorie</value>\n  </data>\n  <data name=\"CategoryEditTitle\" xml:space=\"preserve\">\n    <value>Bewerk Categorietitel</value>\n  </data>\n  <data name=\"CategoryMoveError\" xml:space=\"preserve\">\n    <value>Er is een onbekende fout opgetreden tijdens het verplaatsen van de categorie</value>\n  </data>\n  <data name=\"CensorReplacementChar\" xml:space=\"preserve\">\n    <value>Censuurvervangingskarakter</value>\n  </data>\n  <data name=\"CensorWords\" xml:space=\"preserve\">\n    <value>Censuurwoorden</value>\n  </data>\n  <data name=\"Delete\" xml:space=\"preserve\">\n    <value>Verwijderen</value>\n  </data>\n  <data name=\"DeleteAndBanButton\" xml:space=\"preserve\">\n    <value>Verwijder en Blokeer Gebruiker</value>\n  </data>\n  <data name=\"DeleteUserButton\" xml:space=\"preserve\">\n    <value>Verwijder Gebruiker</value>\n  </data>\n  <data name=\"Description\" xml:space=\"preserve\">\n    <value>Omschrijving</value>\n  </data>\n  <data name=\"Down\" xml:space=\"preserve\">\n    <value>Beneden</value>\n  </data>\n  <data name=\"Edit\" xml:space=\"preserve\">\n    <value>Bewerk</value>\n  </data>\n  <data name=\"EditForum\" xml:space=\"preserve\">\n    <value>Bewerk Forum</value>\n  </data>\n  <data name=\"EditUser\" xml:space=\"preserve\">\n    <value>Bewerk Gebruiker</value>\n  </data>\n  <data name=\"EmailBan\" xml:space=\"preserve\">\n    <value>Blokkeer E-mail</value>\n  </data>\n  <data name=\"EmailIpBan\" xml:space=\"preserve\">\n    <value>Blokkeer E-mail/IP</value>\n  </data>\n  <data name=\"EmailNewOptional\" xml:space=\"preserve\">\n    <value>Nieuwe e-mail (optioneel)</value>\n  </data>\n  <data name=\"EmailUsers\" xml:space=\"preserve\">\n    <value>Mail Gebruikers</value>\n  </data>\n  <data name=\"ErrorLog\" xml:space=\"preserve\">\n    <value>FoutLog</value>\n  </data>\n  <data name=\"Forum\" xml:space=\"preserve\">\n    <value>Forum</value>\n  </data>\n  <data name=\"ForumAdapter\" xml:space=\"preserve\">\n    <value>Forum Adapter (optional, use \"Namespace.Type, AssemblyName\")</value>\n  </data>\n  <data name=\"ForumHome\" xml:space=\"preserve\">\n    <value>Forum Home</value>\n  </data>\n  <data name=\"ForumPermisions\" xml:space=\"preserve\">\n    <value>Forum Permissies</value>\n  </data>\n  <data name=\"ForumSettings\" xml:space=\"preserve\">\n    <value>Forum Instellingen</value>\n  </data>\n  <data name=\"ForumsUncat\" xml:space=\"preserve\">\n    <value>Ongecatagorizeerde  Forums</value>\n  </data>\n  <data name=\"GeneralSettings\" xml:space=\"preserve\">\n    <value>Algemene Instellingen</value>\n  </data>\n  <data name=\"IpBan\" xml:space=\"preserve\">\n    <value>IP Blokkage</value>\n  </data>\n  <data name=\"IpHistory\" xml:space=\"preserve\">\n    <value>IP Historie</value>\n  </data>\n  <data name=\"IsApproved\" xml:space=\"preserve\">\n    <value>Is goedgekeurd</value>\n  </data>\n  <data name=\"IsSubscribed\" xml:space=\"preserve\">\n    <value>Is aangemeld</value>\n  </data>\n  <data name=\"LogErrors\" xml:space=\"preserve\">\n    <value>Log fouten</value>\n  </data>\n  <data name=\"Logging\" xml:space=\"preserve\">\n    <value>Logging</value>\n  </data>\n  <data name=\"LogMod\" xml:space=\"preserve\">\n    <value>Log moderatie</value>\n  </data>\n  <data name=\"LogSecurity\" xml:space=\"preserve\">\n    <value>Log beveiligingsacties</value>\n  </data>\n  <data name=\"MinimumTimeBetweenPosts\" xml:space=\"preserve\">\n    <value>Minimale tijd tussen posts (seconden)</value>\n  </data>\n  <data name=\"ModerationLog\" xml:space=\"preserve\">\n    <value>Moderatie Log</value>\n  </data>\n  <data name=\"Move\" xml:space=\"preserve\">\n    <value>Verplaats</value>\n  </data>\n  <data name=\"NewUserApprovedWithoutVerification\" xml:space=\"preserve\">\n    <value>Nieuwe gebruiker is goedgekeurd zonder verificatie</value>\n  </data>\n  <data name=\"NewUserImageApprovedWithoutMod\" xml:space=\"preserve\">\n    <value>Nieuwe gebruikerafbeelding is goedgekeurd zonder moderatie</value>\n  </data>\n  <data name=\"Parsing\" xml:space=\"preserve\">\n    <value>Parsing</value>\n  </data>\n  <data name=\"PasswordNewOptional\" xml:space=\"preserve\">\n    <value>Nieuw wachtwoord (optioneel)</value>\n  </data>\n  <data name=\"PopForumsAdmin\" xml:space=\"preserve\">\n    <value>POP Forums Beheer</value>\n  </data>\n  <data name=\"PostsPerPage\" xml:space=\"preserve\">\n    <value>Posts per pagina</value>\n  </data>\n  <data name=\"Role\" xml:space=\"preserve\">\n    <value>Rol</value>\n  </data>\n  <data name=\"Roles\" xml:space=\"preserve\">\n    <value>Rollen</value>\n  </data>\n  <data name=\"Security\" xml:space=\"preserve\">\n    <value>Beveiliging</value>\n  </data>\n  <data name=\"SecurityLog\" xml:space=\"preserve\">\n    <value>Beveiligingslog</value>\n  </data>\n  <data name=\"ServerTime\" xml:space=\"preserve\">\n    <value>Server Tijd</value>\n  </data>\n  <data name=\"Services\" xml:space=\"preserve\">\n    <value>Services</value>\n  </data>\n  <data name=\"SessionLength\" xml:space=\"preserve\">\n    <value>Sessielengte (minuten)</value>\n  </data>\n  <data name=\"Size\" xml:space=\"preserve\">\n    <value>Grootte</value>\n  </data>\n  <data name=\"SubjectAndBodyNotEmpty\" xml:space=\"preserve\">\n    <value>Onderwerp en inhoud mogen niet leeg zijn</value>\n  </data>\n  <data name=\"Title\" xml:space=\"preserve\">\n    <value>Titel</value>\n  </data>\n  <data name=\"TopicsPerPage\" xml:space=\"preserve\">\n    <value>Topics per pagina</value>\n  </data>\n  <data name=\"Up\" xml:space=\"preserve\">\n    <value>Omhoog</value>\n  </data>\n  <data name=\"UserAvatarMaxDim\" xml:space=\"preserve\">\n    <value>Gebruiker avatar max dimensies</value>\n  </data>\n  <data name=\"UserDeleteWarning\" xml:space=\"preserve\">\n    <value>WAARSCHUWING! Verwijderen van een account is permanent</value>\n  </data>\n  <data name=\"UserImageApproval\" xml:space=\"preserve\">\n    <value>Gebruikerafbeelding Goedkeuring</value>\n  </data>\n  <data name=\"UserImageMaxDim\" xml:space=\"preserve\">\n    <value>Gebruikerafbeelding max dimensies</value>\n  </data>\n  <data name=\"UserRoles\" xml:space=\"preserve\">\n    <value>Gebruiker Rollen</value>\n  </data>\n  <data name=\"Visible\" xml:space=\"preserve\">\n    <value>Zichtbaar</value>\n  </data>\n  <data name=\"AddNewForum\" xml:space=\"preserve\">\n    <value>Voeg een nieuw forum toe</value>\n  </data>\n  <data name=\"Body\" xml:space=\"preserve\">\n    <value>Inhoud</value>\n  </data>\n  <data name=\"BodyHtml\" xml:space=\"preserve\">\n    <value>HTML Inhoud</value>\n  </data>\n  <data name=\"ClickToLoadMorePosts\" xml:space=\"preserve\">\n    <value>Klik om nieuwere posts op te halen voor dat je antwoordt</value>\n  </data>\n  <data name=\"Comment\" xml:space=\"preserve\">\n    <value>Comment</value>\n  </data>\n  <data name=\"CommentsOptional\" xml:space=\"preserve\">\n    <value>Comments (optineel)</value>\n  </data>\n  <data name=\"CreateJunkWordButton\" xml:space=\"preserve\">\n    <value>Maak Nieuw Junk Woord</value>\n  </data>\n  <data name=\"CreateNewReply\" xml:space=\"preserve\">\n    <value>Maak je antwoord</value>\n  </data>\n  <data name=\"CreateNewRole\" xml:space=\"preserve\">\n    <value>Maak Nieuwe Rol</value>\n  </data>\n  <data name=\"CreateNewTopic\" xml:space=\"preserve\">\n    <value>Maak je nieuwe topic</value>\n  </data>\n  <data name=\"DeleteAllErrors\" xml:space=\"preserve\">\n    <value>Verwijder alle fouten</value>\n  </data>\n  <data name=\"DeleteJunkWordButton\" xml:space=\"preserve\">\n    <value>Verwijder Geselecteerde Junk Woorden</value>\n  </data>\n  <data name=\"DeleteSelectedRole\" xml:space=\"preserve\">\n    <value>Verwijder Geselecteerde Rol</value>\n  </data>\n  <data name=\"EditPost\" xml:space=\"preserve\">\n    <value>Bewerk Post</value>\n  </data>\n  <data name=\"EmailUsersQueued\" xml:space=\"preserve\">\n    <value>E-mail in de wachtrij geplaatst voor de aangemelde gebruikers.</value>\n  </data>\n  <data name=\"EndDate\" xml:space=\"preserve\">\n    <value>Einddatum</value>\n  </data>\n  <data name=\"Event\" xml:space=\"preserve\">\n    <value>Evenement</value>\n  </data>\n  <data name=\"EventTime\" xml:space=\"preserve\">\n    <value>Evenementstijd</value>\n  </data>\n  <data name=\"FavoriteMustBeLoggedIn\" xml:space=\"preserve\">\n    <value>Je moet ingelogd zijn om favoriete topics te kunnen bekijken.</value>\n  </data>\n  <data name=\"FavoritesDontHave\" xml:space=\"preserve\">\n    <value>Je hebt geen favoriete topics.</value>\n  </data>\n  <data name=\"FavoriteTopics\" xml:space=\"preserve\">\n    <value>Favoriete Topics</value>\n  </data>\n  <data name=\"ForumMoveError\" xml:space=\"preserve\">\n    <value>Er is een onbekende fout opgetreden tijdens het verplaatsen van het forum</value>\n  </data>\n  <data name=\"ForumPermissionInstructions\" xml:space=\"preserve\">\n    <value>Om post- en weergaverestricties te verwijderen, verwijder alle rollen van de toepasselijke box.</value>\n  </data>\n  <data name=\"FromEmailAddress\" xml:space=\"preserve\">\n    <value>Van e-mailadres</value>\n  </data>\n  <data name=\"ID\" xml:space=\"preserve\">\n    <value>ID</value>\n  </data>\n  <data name=\"IncludeSignature\" xml:space=\"preserve\">\n    <value>Gebruik handtekening</value>\n  </data>\n  <data name=\"IndexingInterval\" xml:space=\"preserve\">\n    <value>Indexeringsinterval</value>\n  </data>\n  <data name=\"IP\" xml:space=\"preserve\">\n    <value>IP</value>\n  </data>\n  <data name=\"IsRunning\" xml:space=\"preserve\">\n    <value>Loopt</value>\n  </data>\n  <data name=\"JunkWords\" xml:space=\"preserve\">\n    <value>Junk Woorden</value>\n  </data>\n  <data name=\"MarkForumRead\" xml:space=\"preserve\">\n    <value>Markeer forum als gelezen</value>\n  </data>\n  <data name=\"Message\" xml:space=\"preserve\">\n    <value>Bericht</value>\n  </data>\n  <data name=\"Millseconds\" xml:space=\"preserve\">\n    <value>Milliseconden</value>\n  </data>\n  <data name=\"NamePosts\" xml:space=\"preserve\">\n    <value>{0}s posts</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Optional\" xml:space=\"preserve\">\n    <value>(optioneel)</value>\n  </data>\n  <data name=\"PermRoles\" xml:space=\"preserve\">\n    <value>Beheerder en Moderator zijn permanente rollen en kunnen niet verwijderd worden.</value>\n  </data>\n  <data name=\"PostID\" xml:space=\"preserve\">\n    <value>PostID</value>\n  </data>\n  <data name=\"PostingRoles\" xml:space=\"preserve\">\n    <value>Posting Rollen</value>\n  </data>\n  <data name=\"PostNewTopic\" xml:space=\"preserve\">\n    <value>Plaats een nieuw topic</value>\n  </data>\n  <data name=\"PreviewTopic\" xml:space=\"preserve\">\n    <value>Toon voorbeeld van topic</value>\n    <comment>preview the verb, not adjective</comment>\n  </data>\n  <data name=\"Remove\" xml:space=\"preserve\">\n    <value>Verwijder</value>\n  </data>\n  <data name=\"RemoveAll\" xml:space=\"preserve\">\n    <value>Verwijder Alles</value>\n  </data>\n  <data name=\"SendingInterval\" xml:space=\"preserve\">\n    <value>Verzendingsinterval</value>\n  </data>\n  <data name=\"SmtpPassword\" xml:space=\"preserve\">\n    <value>SMTP Wachtwoord</value>\n  </data>\n  <data name=\"SmtpPort\" xml:space=\"preserve\">\n    <value>Poort (standaard is 25)</value>\n  </data>\n  <data name=\"SmtpServer\" xml:space=\"preserve\">\n    <value>SMTP Server</value>\n  </data>\n  <data name=\"SmtpUser\" xml:space=\"preserve\">\n    <value>SMTP Gebruiker</value>\n  </data>\n  <data name=\"StartDate\" xml:space=\"preserve\">\n    <value>Startdatum</value>\n  </data>\n  <data name=\"SubmitNewTopic\" xml:space=\"preserve\">\n    <value>Plaats new topic</value>\n  </data>\n  <data name=\"SubmitReply\" xml:space=\"preserve\">\n    <value>Plaats antwoord</value>\n  </data>\n  <data name=\"TopicID\" xml:space=\"preserve\">\n    <value>TopicID</value>\n  </data>\n  <data name=\"Type\" xml:space=\"preserve\">\n    <value>Type</value>\n  </data>\n  <data name=\"UseEsmtpCred\" xml:space=\"preserve\">\n    <value>Use ESMTP for credentials</value>\n  </data>\n  <data name=\"UserID\" xml:space=\"preserve\">\n    <value>UserID</value>\n  </data>\n  <data name=\"UserIDTarget\" xml:space=\"preserve\">\n    <value>Doel UserID</value>\n  </data>\n  <data name=\"UserImageApprovalNotReq\" xml:space=\"preserve\">\n    <value>Door de gebruiker geuploade afbeeldingen hebben momenteel geen goedkeuring nodig</value>\n  </data>\n  <data name=\"UseSsl\" xml:space=\"preserve\">\n    <value>Gebruik SSL</value>\n  </data>\n  <data name=\"ViewingRoles\" xml:space=\"preserve\">\n    <value>Leesrollen</value>\n  </data>\n  <data name=\"Close\" xml:space=\"preserve\">\n    <value>Sluiten</value>\n  </data>\n  <data name=\"CloseOnReply\" xml:space=\"preserve\">\n    <value>Sluiten bij antwoord</value>\n  </data>\n  <data name=\"FavoriteMake\" xml:space=\"preserve\">\n    <value>Maak Favoriet</value>\n  </data>\n  <data name=\"FavoriteRemove\" xml:space=\"preserve\">\n    <value>Verwijder Van Favorieten</value>\n  </data>\n  <data name=\"Link\" xml:space=\"preserve\">\n    <value>Link</value>\n  </data>\n  <data name=\"Moderator\" xml:space=\"preserve\">\n    <value>Moderator</value>\n  </data>\n  <data name=\"NameAvatar\" xml:space=\"preserve\">\n    <value>{0}s avatar</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"NameLastEdit\" xml:space=\"preserve\">\n    <value>Laatst bijgewerkt door {0}</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Open\" xml:space=\"preserve\">\n    <value>Open</value>\n  </data>\n  <data name=\"Pin\" xml:space=\"preserve\">\n    <value>Pin</value>\n  </data>\n  <data name=\"PostReply\" xml:space=\"preserve\">\n    <value>Post een antwoord</value>\n  </data>\n  <data name=\"Quote\" xml:space=\"preserve\">\n    <value>Citaat</value>\n  </data>\n  <data name=\"RecentTopics\" xml:space=\"preserve\">\n    <value>Recente Topics</value>\n  </data>\n  <data name=\"Reply\" xml:space=\"preserve\">\n    <value>Antwoord</value>\n  </data>\n  <data name=\"ShowMorePosts\" xml:space=\"preserve\">\n    <value>Toon meer posts</value>\n  </data>\n  <data name=\"ShowPreviousPosts\" xml:space=\"preserve\">\n    <value>Toon vorige posts</value>\n  </data>\n  <data name=\"Subscribe\" xml:space=\"preserve\">\n    <value>Aanmelden</value>\n  </data>\n  <data name=\"Undelete\" xml:space=\"preserve\">\n    <value>Verwijderen ongedaan maken</value>\n  </data>\n  <data name=\"Unpin\" xml:space=\"preserve\">\n    <value>Unpin</value>\n  </data>\n  <data name=\"Update\" xml:space=\"preserve\">\n    <value>Update</value>\n  </data>\n  <data name=\"Archive\" xml:space=\"preserve\">\n    <value>Archief</value>\n  </data>\n  <data name=\"NewPM\" xml:space=\"preserve\">\n    <value>Nieuw Privébericht</value>\n  </data>\n  <data name=\"NoResults\" xml:space=\"preserve\">\n    <value>Geen resultaten gevonden</value>\n  </data>\n  <data name=\"Send\" xml:space=\"preserve\">\n    <value>Verstuur</value>\n  </data>\n  <data name=\"Unarchive\" xml:space=\"preserve\">\n    <value>Onarchiveer</value>\n  </data>\n  <data name=\"ViewArchivedMessages\" xml:space=\"preserve\">\n    <value>Toon gearchiveerde berichten</value>\n  </data>\n  <data name=\"DisplayName\" xml:space=\"preserve\">\n    <value>Weergavenaam</value>\n  </data>\n  <data name=\"Error\" xml:space=\"preserve\">\n    <value>Fout</value>\n  </data>\n  <data name=\"ErrorSettingUpDb\" xml:space=\"preserve\">\n    <value>Er is een fout opgetreden bij het instellen van je database</value>\n  </data>\n  <data name=\"ForumReady\" xml:space=\"preserve\">\n    <value>Je forum is klaar.</value>\n  </data>\n  <data name=\"ForumTitle\" xml:space=\"preserve\">\n    <value>Forumtitel</value>\n  </data>\n  <data name=\"GoAdmin\" xml:space=\"preserve\">\n    <value>Ga naar de beheersecetie om het instellen van je forum af te ronden</value>\n  </data>\n  <data name=\"InvalidLink\" xml:space=\"preserve\">\n    <value>Die link is niet meer geldig.</value>\n  </data>\n  <data name=\"NoDataConnection\" xml:space=\"preserve\">\n    <value>Geen dataconnectie</value>\n  </data>\n  <data name=\"PopForumsSetup\" xml:space=\"preserve\">\n    <value>POP Forums Setup</value>\n  </data>\n  <data name=\"SetupCantConnect\" xml:space=\"preserve\">\n    <value>De setup kan de data store niet benaderen. Controleer a.u.b. de connection string en de configuratie.</value>\n  </data>\n  <data name=\"SetupConnSuccess\" xml:space=\"preserve\">\n    <value>A database connection has been successful. Please fill out the following values to get started</value>\n  </data>\n  <data name=\"SetupDatabase\" xml:space=\"preserve\">\n    <value>Stel de database in</value>\n  </data>\n  <data name=\"SetupFirstUser\" xml:space=\"preserve\">\n    <value>Maak je eerste gebruiker aan (die zal beheer- en moderatorrechten krijgen)</value>\n  </data>\n  <data name=\"SubscribedTopics\" xml:space=\"preserve\">\n    <value>Aangemelde Topics</value>\n  </data>\n  <data name=\"SubscribeLoggedIn\" xml:space=\"preserve\">\n    <value>Je moet ingelogd zijn om aangemelde topics te kunnen zien.</value>\n  </data>\n  <data name=\"SubscribeNone\" xml:space=\"preserve\">\n    <value>Je bent niet aangemeld voor een topic.</value>\n  </data>\n  <data name=\"Success\" xml:space=\"preserve\">\n    <value>Succesvol!</value>\n  </data>\n  <data name=\"UnsubscribeTopic\" xml:space=\"preserve\">\n    <value>{0} is afgemeld van {1}</value>\n    <comment>{0} is a name, {1} is a topic title</comment>\n  </data>\n  <data name=\"ArchivedNewPosts\" xml:space=\"preserve\">\n    <value>Gearchiveerd met nieuwe posts</value>\n  </data>\n  <data name=\"Closed\" xml:space=\"preserve\">\n    <value>Topic gesloten</value>\n  </data>\n  <data name=\"ClosedPinned\" xml:space=\"preserve\">\n    <value>Gesloten, gepind topic</value>\n  </data>\n  <data name=\"DeleteTopic\" xml:space=\"preserve\">\n    <value>Verwijder Topic</value>\n  </data>\n  <data name=\"First\" xml:space=\"preserve\">\n    <value>Eerste</value>\n  </data>\n  <data name=\"ForgotPasswordEmail\" xml:space=\"preserve\">\n    <value>Om je wachtwoord te resetten op {0}, volg a.u.b. deze link:{3}{1}{3}{3}{2}</value>\n    <comment>{0}=title of forum, {1}=email link, {2}=signature, {3}=new line</comment>\n  </data>\n  <data name=\"ForgotPasswordSubject\" xml:space=\"preserve\">\n    <value>Je wachtwoordresetinstructies voor {0}</value>\n    <comment>{0} is name of forum</comment>\n  </data>\n  <data name=\"ForumNoPost\" xml:space=\"preserve\">\n    <value>Je hebt geen toestemming om in dit forum te posten</value>\n  </data>\n  <data name=\"ForumNoView\" xml:space=\"preserve\">\n    <value>Je hebt geen toestemming om dit forum te bekijken</value>\n  </data>\n  <data name=\"LessThanMinute\" xml:space=\"preserve\">\n    <value>Minder dan een minuut geleden</value>\n  </data>\n  <data name=\"LoginToPost\" xml:space=\"preserve\">\n    <value>Je dient ingelogd te zijn om te kunnen posten</value>\n  </data>\n  <data name=\"MinutesAgo\" xml:space=\"preserve\">\n    <value>{0} minuten geleden</value>\n    <comment>{0} is a number</comment>\n  </data>\n  <data name=\"More\" xml:space=\"preserve\">\n    <value>Meer</value>\n  </data>\n  <data name=\"NewPosts\" xml:space=\"preserve\">\n    <value>Nieuwe posts</value>\n  </data>\n  <data name=\"NewPostsClosed\" xml:space=\"preserve\">\n    <value>Nieuwe posts, gesloten topic</value>\n  </data>\n  <data name=\"NewPostsClosedPinned\" xml:space=\"preserve\">\n    <value>Nieuwe posts, gesloten &amp; gepinde topic</value>\n  </data>\n  <data name=\"NewPostsPinned\" xml:space=\"preserve\">\n    <value>Nieuwe posts, gepinde topic</value>\n  </data>\n  <data name=\"Next\" xml:space=\"preserve\">\n    <value>Volgende</value>\n  </data>\n  <data name=\"NoNewPosts\" xml:space=\"preserve\">\n    <value>Geen nieuwe posts</value>\n  </data>\n  <data name=\"NotLoggedIn\" xml:space=\"preserve\">\n    <value>Je bent niet ingelogd</value>\n  </data>\n  <data name=\"OneMinuteAgo\" xml:space=\"preserve\">\n    <value>1 minuut geleden</value>\n  </data>\n  <data name=\"Pinned\" xml:space=\"preserve\">\n    <value>Gepinde topic</value>\n  </data>\n  <data name=\"PMCreateWarnings\" xml:space=\"preserve\">\n\t  <value>Je moet een bericht en op z'n minst één gebruiker toevoegen</value>\n  </data>\n  <data name=\"PostEmpty\" xml:space=\"preserve\">\n    <value>Je kunt geen lege post maken</value>\n  </data>\n  <data name=\"PostWait\" xml:space=\"preserve\">\n    <value>Je dient {0} seconden te wachten tussen je posts en je mag je laatste post niet herposten</value>\n    <comment>{0} is a number for seconds</comment>\n  </data>\n  <data name=\"Previous\" xml:space=\"preserve\">\n    <value>Vorige</value>\n  </data>\n  <data name=\"RegisterEmailSubject\" xml:space=\"preserve\">\n    <value>Forum registratie - {0}</value>\n    <comment>{0} is name of forum</comment>\n  </data>\n  <data name=\"RegisterEmailThankYou\" xml:space=\"preserve\">\n    <value>Dank je voor je registratie bij {0}.{2}{2}{1}</value>\n    <comment>{0}=name of forum, {1}=forum link, {2}=new line</comment>\n  </data>\n  <data name=\"RegisterEmailThankYouVerify\" xml:space=\"preserve\">\n    <value>Dank je voor je registratie bij {0}. Klikt op de deze link om je registratie te bevestigen :{5}{5}{1} {5}{5}Als de link niet werkt, bezoek dan de volgende pagina, en voer de bevestigingscode in:{5}{5}Link: {2} {5}Code: {3} {5}{5}{4}</value>\n    <comment>{0}=name of forum, {1}=auth link, {2}=verify link, {3}=auth code, {4}=sig, {5}new line</comment>\n  </data>\n  <data name=\"SettingsSaved\" xml:space=\"preserve\">\n    <value>Instellingen opgeslagen</value>\n  </data>\n  <data name=\"TodayTime\" xml:space=\"preserve\">\n    <value>Vandaag, {0}</value>\n    <comment>{0} is a time like 1:43pm</comment>\n  </data>\n  <data name=\"TopicNotExist\" xml:space=\"preserve\">\n    <value>Die topic bestaat niet</value>\n  </data>\n  <data name=\"YesterdayTime\" xml:space=\"preserve\">\n    <value>Gisteren, {0}</value>\n    <comment>{0} a time like 1:43pm</comment>\n  </data>\n  <data name=\"Loading\" xml:space=\"preserve\">\n    <value>Laden</value>\n  </data>\n  <data name=\"Voted\" xml:space=\"preserve\">\n    <value>Gestemd</value>\n  </data>\n  <data name=\"VoteUpPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; gestemd op een post in het topic: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"ScoringGame\" xml:space=\"preserve\">\n    <value>Scoren spel</value>\n  </data>\n  <data name=\"None\" xml:space=\"preserve\">\n    <value>Geen</value>\n  </data>\n  <data name=\"ActivityFeed\" xml:space=\"preserve\">\n    <value>Activiteitentoevoer</value>\n  </data>\n  <data name=\"NewReplyPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; maakte een bericht in het onderwerp: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"NewPostPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; begon een nieuw onderwerp: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to topic{3} is topic name</comment>\n  </data>\n  <data name=\"ExternalLogins\" xml:space=\"preserve\">\n    <value>Externe Logins</value>\n  </data>\n  <data name=\"UseExistingForumAccount\" xml:space=\"preserve\">\n    <value>Gebruik bestaande forum gehouden</value>\n  </data>\n  <data name=\"ExpiredLogin\" xml:space=\"preserve\">\n    <value>Dat login is verlopen of niet geldig is</value>\n  </data>\n  <data name=\"ExternalLoginsDisabled\" xml:space=\"preserve\">\n    <value>Externe logins zijn uitgeschakeld</value>\n  </data>\n  <data name=\"NoExternalLoginsRegistered\" xml:space=\"preserve\">\n    <value>Je hebt geen externe logins geregistreerd</value>\n  </data>\n  <data name=\"DeletePermanently\" xml:space=\"preserve\">\n    <value>Permanent verwijderen</value>\n  </data>\n  <data name=\"Preview\" xml:space=\"preserve\">\n    <value>Voorbeeld</value>\n  </data>\n  <data name=\"AskAquestion\" xml:space=\"preserve\">\n    <value />\n  </data>\n  <data name=\"Answers\" xml:space=\"preserve\">\n    <value>Antwoorden</value>\n  </data>\n  <data name=\"ChooseAnswer\" xml:space=\"preserve\">\n    <value>Kies antwoord</value>\n  </data>\n  <data name=\"PostAnswer\" xml:space=\"preserve\">\n    <value>bericht antwoord</value>\n  </data>\n  <data name=\"QuestionAnswered\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; koos voor een antwoord op de vraag: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"SubmitAnswer\" xml:space=\"preserve\">\n    <value>submit antwoord</value>\n  </data>\n  <data name=\"AppRestartRequired\" xml:space=\"preserve\">\n    <value>App herstart vereist</value>\n  </data>\n  <data name=\"SearchError\" xml:space=\"preserve\">\n    <value>Er is een zoekfout opgetreden</value>\n  </data>\n  <data name=\"CloseOldTopics\" xml:space=\"preserve\">\n    <value>Sluit oude onderwerpen</value>\n  </data>\n  <data name=\"Days\" xml:space=\"preserve\">\n    <value>dagen</value>\n  </data>\n  <data name=\"PrivateForum\" xml:space=\"preserve\">\n    <value>Privé forum</value>\n  </data>\n  <data name=\"AccountNotVerified\" xml:space=\"preserve\">\n    <value>Kontoen er ikke bekreftet</value>\n  </data>\n  <data name=\"SelectText\" xml:space=\"preserve\">\n\t  <value>Selecteer tekst</value>\n  </data>\n  <data name=\"Notifications\" xml:space=\"preserve\">\n\t  <value>Meldingen</value>\n  </data>\n  <data name=\"NewReplyNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; maakte een bericht in het onderwerp: &lt;b&gt;{1}&lt;/b&gt;\n</value>\n  </data>\n  <data name=\"VoteUpNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; gestemd op een post in het topic: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"QuestionAnsweredNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; koos voor een antwoord op de vraag: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"AutoFollow\" xml:space=\"preserve\">\n\t  <value>Volg automatisch onderwerpen waarop u reageert</value>\n  </data>\n  <data name=\"MarkAllRead\" xml:space=\"preserve\">\n\t  <value>Markeer alles als gelezen</value>\n  </data>\n  <data name=\"UploadImage\" xml:space=\"preserve\">\n\t  <value>Afbeelding uploaden</value>\n  </data>\n  <data name=\"Ignored\" xml:space=\"preserve\">\n\t  <value>Genegeerd</value>\n  </data>\n  <data name=\"IgnoreList\" xml:space=\"preserve\">\n\t  <value>Lijst negeren</value>\n  </data>\n  <data name=\"Ignore\" xml:space=\"preserve\">\n\t  <value>Negeren</value>\n  </data>\n</root>"
  },
  {
    "path": "src/PopForums/Resources/Resources.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"Account\" xml:space=\"preserve\">\n    <value>Account</value>\n  </data>\n  <data name=\"AccountCreated\" xml:space=\"preserve\">\n    <value>Account Created</value>\n  </data>\n  <data name=\"AccountReady\" xml:space=\"preserve\">\n    <value>Your account was created and you may begin using the forum.</value>\n  </data>\n  <data name=\"AccountReadyCheckEmail\" xml:space=\"preserve\">\n    <value>Please check your e-mail for a verification link to begin using the forum.</value>\n  </data>\n  <data name=\"AccountVerified\" xml:space=\"preserve\">\n    <value>Your account has been verified</value>\n  </data>\n  <data name=\"AlreadyCreatedAccount\" xml:space=\"preserve\">\n    <value>You are already logged in and have an account.</value>\n  </data>\n  <data name=\"AsYouWouldLikeItToAppear\" xml:space=\"preserve\">\n    <value>(as you would like it to appear)</value>\n  </data>\n  <data name=\"AtLeastSixChar\" xml:space=\"preserve\">\n    <value>(at least six characters)</value>\n  </data>\n  <data name=\"By\" xml:space=\"preserve\">\n    <value>by</value>\n    <comment>context: \"Last post: 12am by Jeff\"</comment>\n  </data>\n  <data name=\"CreateAccountButton\" xml:space=\"preserve\">\n    <value>Create Account</value>\n  </data>\n  <data name=\"CreateAnAccount\" xml:space=\"preserve\">\n    <value>Create an account</value>\n  </data>\n  <data name=\"EditYourProfile\" xml:space=\"preserve\">\n    <value>Edit your profile</value>\n  </data>\n  <data name=\"Email\" xml:space=\"preserve\">\n    <value>E-mail</value>\n  </data>\n  <data name=\"EmailProblemAccount\" xml:space=\"preserve\">\n    <value>There was a problem sending your verification e-mail: </value>\n  </data>\n  <data name=\"Favorites\" xml:space=\"preserve\">\n    <value>Favorites</value>\n  </data>\n  <data name=\"Forbidden\" xml:space=\"preserve\">\n    <value>Sorry, you don't have permission to view this.</value>\n  </data>\n  <data name=\"Forums\" xml:space=\"preserve\">\n    <value>Forums</value>\n  </data>\n  <data name=\"HaveReadTOS\" xml:space=\"preserve\">\n    <value>I have read and accept the terms of service</value>\n  </data>\n  <data name=\"Last\" xml:space=\"preserve\">\n    <value>Last</value>\n  </data>\n  <data name=\"Login\" xml:space=\"preserve\">\n    <value>Login</value>\n  </data>\n  <data name=\"Logout\" xml:space=\"preserve\">\n    <value>Logout</value>\n  </data>\n  <data name=\"MarkAllForumsRead\" xml:space=\"preserve\">\n    <value>Mark all forums read</value>\n  </data>\n  <data name=\"MyBirthdayIsOnOrBefore\" xml:space=\"preserve\">\n    <value>My birthday is on or before {0}</value>\n    <comment>where {0} is a date</comment>\n  </data>\n  <data name=\"Name\" xml:space=\"preserve\">\n    <value>Name</value>\n  </data>\n  <data name=\"NeedToVerifyExistingAccount\" xml:space=\"preserve\">\n    <value>Need to verify an existing account?</value>\n  </data>\n  <data name=\"PageNotFound\" xml:space=\"preserve\">\n    <value>Page not found</value>\n  </data>\n  <data name=\"Password\" xml:space=\"preserve\">\n    <value>Password</value>\n  </data>\n  <data name=\"Posts\" xml:space=\"preserve\">\n    <value>Posts</value>\n  </data>\n  <data name=\"PrivateMessages\" xml:space=\"preserve\">\n    <value>Private Messages</value>\n  </data>\n  <data name=\"Recent\" xml:space=\"preserve\">\n    <value>Recent</value>\n  </data>\n  <data name=\"RegisteredUsers\" xml:space=\"preserve\">\n    <value>Registered users</value>\n  </data>\n  <data name=\"RetypePassword\" xml:space=\"preserve\">\n    <value>Retype password</value>\n  </data>\n  <data name=\"Search\" xml:space=\"preserve\">\n    <value>Search</value>\n  </data>\n  <data name=\"SubscribeToList\" xml:space=\"preserve\">\n    <value>Subscribe to mailing list</value>\n  </data>\n  <data name=\"Subscriptions\" xml:space=\"preserve\">\n    <value>Subscriptions</value>\n  </data>\n  <data name=\"TermsOfService\" xml:space=\"preserve\">\n    <value>Terms of service</value>\n  </data>\n  <data name=\"TimeZone\" xml:space=\"preserve\">\n    <value>Time zone</value>\n  </data>\n  <data name=\"Topics\" xml:space=\"preserve\">\n    <value>Topics</value>\n  </data>\n  <data name=\"Total\" xml:space=\"preserve\">\n    <value>Total</value>\n  </data>\n  <data name=\"TotalPosts\" xml:space=\"preserve\">\n    <value>Total posts</value>\n  </data>\n  <data name=\"TotalTopics\" xml:space=\"preserve\">\n    <value>Total topics</value>\n  </data>\n  <data name=\"UseDaylight\" xml:space=\"preserve\">\n    <value>Use N.America/Europe Daylight Saving</value>\n  </data>\n  <data name=\"UsersOnline\" xml:space=\"preserve\">\n    <value>Users Online</value>\n  </data>\n  <data name=\"MustBe13\" xml:space=\"preserve\">\n    <value>You must be 13 or older to register</value>\n  </data>\n  <data name=\"MustAcceptTOS\" xml:space=\"preserve\">\n    <value>You must accept the terms of service</value>\n  </data>\n  <data name=\"RetypeYourPassword\" xml:space=\"preserve\">\n    <value>Retype your password</value>\n  </data>\n  <data name=\"NameRequired\" xml:space=\"preserve\">\n    <value>Name required</value>\n  </data>\n  <data name=\"NameInUse\" xml:space=\"preserve\">\n    <value>That name is already in use</value>\n  </data>\n  <data name=\"EmailRequired\" xml:space=\"preserve\">\n    <value>E-mail required</value>\n  </data>\n  <data name=\"ValidEmailAddressRequired\" xml:space=\"preserve\">\n    <value>Valid e-mail address required</value>\n  </data>\n  <data name=\"EmailInUse\" xml:space=\"preserve\">\n    <value>That e-mail address is already in use</value>\n  </data>\n  <data name=\"EmailBanned\" xml:space=\"preserve\">\n    <value>That e-mail address has been banned</value>\n  </data>\n  <data name=\"IPBanned\" xml:space=\"preserve\">\n    <value>Your IP address has been banned</value>\n  </data>\n  <data name=\"RetypePasswordMustMatch\" xml:space=\"preserve\">\n    <value>Retyped password must match</value>\n  </data>\n  <data name=\"EditAccount\" xml:space=\"preserve\">\n    <value>Edit Account</value>\n  </data>\n  <data name=\"EditProfile\" xml:space=\"preserve\">\n    <value>Edit Profile</value>\n  </data>\n  <data name=\"EnterVerificationCode\" xml:space=\"preserve\">\n    <value>Please enter your verification code</value>\n  </data>\n  <data name=\"ForumHomePage\" xml:space=\"preserve\">\n    <value>Forum home page</value>\n  </data>\n  <data name=\"MustBeRegisteredToEditAccount\" xml:space=\"preserve\">\n    <value>You must be registered and logged in to edit your account.</value>\n  </data>\n  <data name=\"NoUserFoundWithEmail\" xml:space=\"preserve\">\n    <value>No user found with that e-mail</value>\n  </data>\n  <data name=\"SendEmailWithNewCodeButton\" xml:space=\"preserve\">\n    <value>Send E-mail With New Code</value>\n  </data>\n  <data name=\"VerificationEmailSent\" xml:space=\"preserve\">\n    <value>Your verification e-mail has been sent</value>\n  </data>\n  <data name=\"VerificationFailure\" xml:space=\"preserve\">\n    <value>Verification Failure</value>\n  </data>\n  <data name=\"VerificationIfYouNeed\" xml:space=\"preserve\">\n    <value>If you need a new verification code, enter your e-mail here</value>\n  </data>\n  <data name=\"VerificationLinkBad\" xml:space=\"preserve\">\n    <value>Sorry, the verification link you followed or code you entered is not valid.</value>\n  </data>\n  <data name=\"VerifyAccount\" xml:space=\"preserve\">\n    <value>Verify Account</value>\n  </data>\n  <data name=\"VerifyCodeButton\" xml:space=\"preserve\">\n    <value>Verify Code</value>\n  </data>\n  <data name=\"Avatar\" xml:space=\"preserve\">\n    <value>Avatar</value>\n  </data>\n  <data name=\"AvatarDelete\" xml:space=\"preserve\">\n    <value>Delete this avatar</value>\n  </data>\n  <data name=\"ChangeEmail\" xml:space=\"preserve\">\n    <value>Change E-mail</value>\n  </data>\n  <data name=\"ChangeEmailButton\" xml:space=\"preserve\">\n    <value>Change e-mail and send code</value>\n  </data>\n  <data name=\"ChangeEmailConsequence\" xml:space=\"preserve\">\n    <value>Changing your e-mail address will cause a new verification code to be generated and sent to the new e-mail address. You will not be able to post until you verify your e-mail address with the new code.</value>\n  </data>\n  <data name=\"ChangePassword\" xml:space=\"preserve\">\n    <value>Change Password</value>\n  </data>\n  <data name=\"ChangeYourEmailPassword\" xml:space=\"preserve\">\n    <value>Change your e-mail or password</value>\n  </data>\n  <data name=\"DateOfBirth\" xml:space=\"preserve\">\n    <value>Date of birth</value>\n  </data>\n  <data name=\"Details\" xml:space=\"preserve\">\n    <value>Details</value>\n  </data>\n  <data name=\"EmailChangeSuccess\" xml:space=\"preserve\">\n    <value>Your e-mail was changed successfully</value>\n  </data>\n  <data name=\"EmailNew\" xml:space=\"preserve\">\n    <value>New e-mail address</value>\n  </data>\n  <data name=\"EmailNewRetype\" xml:space=\"preserve\">\n    <value>Retype new e-mail address</value>\n  </data>\n  <data name=\"EmailNotFound\" xml:space=\"preserve\">\n    <value>E-mail not found</value>\n  </data>\n  <data name=\"EmailSent\" xml:space=\"preserve\">\n    <value>Your e-mail has been sent</value>\n  </data>\n  <data name=\"EmailsMustMatch\" xml:space=\"preserve\">\n    <value>Those e-mail addresses don't match</value>\n  </data>\n  <data name=\"EmailUser\" xml:space=\"preserve\">\n    <value>E-mail User</value>\n  </data>\n  <data name=\"ForcePlainTextBox\" xml:space=\"preserve\">\n    <value>Force plain text boxes</value>\n  </data>\n  <data name=\"ForgotInstructions\" xml:space=\"preserve\">\n    <value>Enter the e-mail address associated with your account to receive instructions for resetting your password:</value>\n  </data>\n  <data name=\"ForgotInstructionsSent\" xml:space=\"preserve\">\n    <value>Instructions sent to your e-mail</value>\n  </data>\n  <data name=\"ForgotPassword\" xml:space=\"preserve\">\n    <value>Forgot Password</value>\n  </data>\n  <data name=\"ForgotPasswordQuestion\" xml:space=\"preserve\">\n    <value>Forgot your password?</value>\n  </data>\n  <data name=\"HideVanity\" xml:space=\"preserve\">\n    <value>Hide vanity</value>\n  </data>\n  <data name=\"In\" xml:space=\"preserve\">\n    <value>in</value>\n    <comment>context: started by Jeff in General Forum</comment>\n  </data>\n  <data name=\"Joined\" xml:space=\"preserve\">\n    <value>Joined</value>\n  </data>\n  <data name=\"Location\" xml:space=\"preserve\">\n    <value>Location</value>\n  </data>\n  <data name=\"LoginAlready\" xml:space=\"preserve\">\n    <value>You are already logged in.</value>\n  </data>\n  <data name=\"LoginBad\" xml:space=\"preserve\">\n    <value>Bad e-mail or password</value>\n  </data>\n  <data name=\"ManagePhotos\" xml:space=\"preserve\">\n    <value>Manage Photos</value>\n  </data>\n  <data name=\"ManageYourPhotos\" xml:space=\"preserve\">\n    <value>Manage your photos</value>\n  </data>\n  <data name=\"NewPasswordSaved\" xml:space=\"preserve\">\n    <value>New password saved</value>\n  </data>\n  <data name=\"NotRegisteredQuestion\" xml:space=\"preserve\">\n    <value>Not registered?</value>\n  </data>\n  <data name=\"OldPasswordIncorrect\" xml:space=\"preserve\">\n    <value>Old password incorrect</value>\n  </data>\n  <data name=\"Options\" xml:space=\"preserve\">\n    <value>Options</value>\n  </data>\n  <data name=\"PasswordNew\" xml:space=\"preserve\">\n    <value>New password</value>\n  </data>\n  <data name=\"PasswordNewRetype\" xml:space=\"preserve\">\n    <value>Retype new password</value>\n  </data>\n  <data name=\"PasswordOld\" xml:space=\"preserve\">\n    <value>Old password</value>\n  </data>\n  <data name=\"PasswordReset\" xml:space=\"preserve\">\n    <value>Reset Password</value>\n  </data>\n  <data name=\"PasswordResetLinkInvalid\" xml:space=\"preserve\">\n    <value>This is not a valid password reset link.</value>\n  </data>\n  <data name=\"PasswordResetNote\" xml:space=\"preserve\">\n    <value>Your new password is active and you're now logged in.</value>\n  </data>\n  <data name=\"PasswordResetSuccess\" xml:space=\"preserve\">\n    <value>Password Reset Successful</value>\n  </data>\n  <data name=\"Photo\" xml:space=\"preserve\">\n    <value>Photo</value>\n  </data>\n  <data name=\"PhotoDelete\" xml:space=\"preserve\">\n    <value>Delete this photo</value>\n  </data>\n  <data name=\"PhotoNotApproved\" xml:space=\"preserve\">\n    <value>(this image has not yet been approved by an administrator)</value>\n  </data>\n  <data name=\"ProfileUpdated\" xml:space=\"preserve\">\n    <value>Profile updated</value>\n  </data>\n  <data name=\"RememberMe\" xml:space=\"preserve\">\n    <value>Remember me?</value>\n  </data>\n  <data name=\"Replies\" xml:space=\"preserve\">\n    <value>Replies</value>\n  </data>\n  <data name=\"Save\" xml:space=\"preserve\">\n    <value>Save</value>\n  </data>\n  <data name=\"SendEmailButton\" xml:space=\"preserve\">\n    <value>Send E-mail</value>\n  </data>\n  <data name=\"SendPM\" xml:space=\"preserve\">\n    <value>Send Private Message</value>\n  </data>\n  <data name=\"ShowProfileDetails\" xml:space=\"preserve\">\n    <value>Show your profile details (but not e-mail)</value>\n  </data>\n  <data name=\"Signature\" xml:space=\"preserve\">\n    <value>Signature</value>\n  </data>\n  <data name=\"StartedBy\" xml:space=\"preserve\">\n    <value>Started by</value>\n  </data>\n  <data name=\"Subject\" xml:space=\"preserve\">\n    <value>Subject</value>\n  </data>\n  <data name=\"Text\" xml:space=\"preserve\">\n    <value>Text</value>\n  </data>\n  <data name=\"To\" xml:space=\"preserve\">\n    <value>To</value>\n  </data>\n  <data name=\"UploadNew\" xml:space=\"preserve\">\n    <value>Upload new</value>\n  </data>\n  <data name=\"UserNotFound\" xml:space=\"preserve\">\n    <value>User not found</value>\n  </data>\n  <data name=\"Views\" xml:space=\"preserve\">\n    <value>Views</value>\n  </data>\n  <data name=\"Web\" xml:space=\"preserve\">\n    <value>Web</value>\n  </data>\n  <data name=\"WebVisit\" xml:space=\"preserve\">\n    <value>Visit user Web site</value>\n  </data>\n  <data name=\"YourIP\" xml:space=\"preserve\">\n    <value>Your IP address</value>\n  </data>\n  <data name=\"Birthday\" xml:space=\"preserve\">\n    <value>Birthday</value>\n  </data>\n  <data name=\"Contact\" xml:space=\"preserve\">\n    <value>Contact</value>\n  </data>\n  <data name=\"Profile\" xml:space=\"preserve\">\n    <value>Profile</value>\n  </data>\n  <data name=\"SendNameEmail\" xml:space=\"preserve\">\n    <value>Send {0} an e-mail message</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"SendNamePM\" xml:space=\"preserve\">\n    <value>Send {0} a private message</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Unsubscribe\" xml:space=\"preserve\">\n    <value>Unsubscribe</value>\n  </data>\n  <data name=\"UnsubscribeFail\" xml:space=\"preserve\">\n    <value>Unsubscribe Failure</value>\n  </data>\n  <data name=\"UnsubscribeLinkBad\" xml:space=\"preserve\">\n    <value>The link you followed is not valid.</value>\n  </data>\n  <data name=\"UnsubscribeNote\" xml:space=\"preserve\">\n    <value>Your unsubscribe request has been processed. You can re-subscribe by logging in and going to your account page.</value>\n  </data>\n  <data name=\"AddForum\" xml:space=\"preserve\">\n    <value>Add Forum</value>\n  </data>\n  <data name=\"AddNew\" xml:space=\"preserve\">\n    <value>Add New</value>\n  </data>\n  <data name=\"AllowImages\" xml:space=\"preserve\">\n    <value>Allow images</value>\n  </data>\n  <data name=\"Archived\" xml:space=\"preserve\">\n    <value>Archived</value>\n  </data>\n  <data name=\"AreYouSure\" xml:space=\"preserve\">\n    <value>Are you sure?</value>\n  </data>\n  <data name=\"Ban\" xml:space=\"preserve\">\n    <value>Ban</value>\n  </data>\n  <data name=\"BanRemove\" xml:space=\"preserve\">\n    <value>Remove Ban</value>\n  </data>\n  <data name=\"Categories\" xml:space=\"preserve\">\n    <value>Categories</value>\n  </data>\n  <data name=\"Category\" xml:space=\"preserve\">\n    <value>Category</value>\n  </data>\n  <data name=\"CategoryEditTitle\" xml:space=\"preserve\">\n    <value>Edit Category Title</value>\n  </data>\n  <data name=\"CategoryMoveError\" xml:space=\"preserve\">\n    <value>There was an unknown error while attempting to move category</value>\n  </data>\n  <data name=\"CensorReplacementChar\" xml:space=\"preserve\">\n    <value>Censor replacement character</value>\n  </data>\n  <data name=\"CensorWords\" xml:space=\"preserve\">\n    <value>Censor words</value>\n  </data>\n  <data name=\"Delete\" xml:space=\"preserve\">\n    <value>Delete</value>\n  </data>\n  <data name=\"DeleteAndBanButton\" xml:space=\"preserve\">\n    <value>Delete and Ban User</value>\n  </data>\n  <data name=\"DeleteUserButton\" xml:space=\"preserve\">\n    <value>Delete User</value>\n  </data>\n  <data name=\"Description\" xml:space=\"preserve\">\n    <value>Description</value>\n  </data>\n  <data name=\"Down\" xml:space=\"preserve\">\n    <value>Down</value>\n  </data>\n  <data name=\"Edit\" xml:space=\"preserve\">\n    <value>Edit</value>\n  </data>\n  <data name=\"EditForum\" xml:space=\"preserve\">\n    <value>Edit Forum</value>\n  </data>\n  <data name=\"EditUser\" xml:space=\"preserve\">\n    <value>Edit User</value>\n  </data>\n  <data name=\"EmailBan\" xml:space=\"preserve\">\n    <value>E-mail Ban</value>\n  </data>\n  <data name=\"EmailIpBan\" xml:space=\"preserve\">\n    <value>E-mail/IP Ban</value>\n  </data>\n  <data name=\"EmailNewOptional\" xml:space=\"preserve\">\n    <value>New e-mail (optional)</value>\n  </data>\n  <data name=\"EmailUsers\" xml:space=\"preserve\">\n    <value>E-mail Users</value>\n  </data>\n  <data name=\"ErrorLog\" xml:space=\"preserve\">\n    <value>ErrorLog</value>\n  </data>\n  <data name=\"Forum\" xml:space=\"preserve\">\n    <value>Forum</value>\n  </data>\n  <data name=\"ForumAdapter\" xml:space=\"preserve\">\n    <value>Forum Adapter (optional, use \"Namespace.Type, AssemblyName\")</value>\n  </data>\n  <data name=\"ForumHome\" xml:space=\"preserve\">\n    <value>Forum Home</value>\n  </data>\n  <data name=\"ForumPermisions\" xml:space=\"preserve\">\n    <value>Forum Permissions</value>\n  </data>\n  <data name=\"ForumSettings\" xml:space=\"preserve\">\n    <value>Forum Settings</value>\n  </data>\n  <data name=\"ForumsUncat\" xml:space=\"preserve\">\n    <value>Uncategorized Forums</value>\n  </data>\n  <data name=\"GeneralSettings\" xml:space=\"preserve\">\n    <value>General Settings</value>\n  </data>\n  <data name=\"IpBan\" xml:space=\"preserve\">\n    <value>IP Ban</value>\n  </data>\n  <data name=\"IpHistory\" xml:space=\"preserve\">\n    <value>IP History</value>\n  </data>\n  <data name=\"IsApproved\" xml:space=\"preserve\">\n    <value>Is approved</value>\n  </data>\n  <data name=\"IsSubscribed\" xml:space=\"preserve\">\n    <value>Is subscribed</value>\n  </data>\n  <data name=\"LogErrors\" xml:space=\"preserve\">\n    <value>Log errors</value>\n  </data>\n  <data name=\"Logging\" xml:space=\"preserve\">\n    <value>Logging</value>\n  </data>\n  <data name=\"LogMod\" xml:space=\"preserve\">\n    <value>Log moderation</value>\n  </data>\n  <data name=\"LogSecurity\" xml:space=\"preserve\">\n    <value>Log security actions</value>\n  </data>\n  <data name=\"MinimumTimeBetweenPosts\" xml:space=\"preserve\">\n    <value>Minimum time between posts (seconds)</value>\n  </data>\n  <data name=\"ModerationLog\" xml:space=\"preserve\">\n    <value>Moderation Log</value>\n  </data>\n  <data name=\"Move\" xml:space=\"preserve\">\n    <value>Move</value>\n  </data>\n  <data name=\"NewUserApprovedWithoutVerification\" xml:space=\"preserve\">\n    <value>New user is approved without verification</value>\n  </data>\n  <data name=\"NewUserImageApprovedWithoutMod\" xml:space=\"preserve\">\n    <value>New user image approved without moderation</value>\n  </data>\n  <data name=\"Parsing\" xml:space=\"preserve\">\n    <value>Parsing</value>\n  </data>\n  <data name=\"PasswordNewOptional\" xml:space=\"preserve\">\n    <value>New password (optional)</value>\n  </data>\n  <data name=\"PopForumsAdmin\" xml:space=\"preserve\">\n    <value>POP Forums Administration</value>\n  </data>\n  <data name=\"PostsPerPage\" xml:space=\"preserve\">\n    <value>Posts per page</value>\n  </data>\n  <data name=\"Role\" xml:space=\"preserve\">\n    <value>Role</value>\n  </data>\n  <data name=\"Roles\" xml:space=\"preserve\">\n    <value>Roles</value>\n  </data>\n  <data name=\"Security\" xml:space=\"preserve\">\n    <value>Security</value>\n  </data>\n  <data name=\"SecurityLog\" xml:space=\"preserve\">\n    <value>Security Log</value>\n  </data>\n  <data name=\"ServerTime\" xml:space=\"preserve\">\n    <value>Server Time</value>\n  </data>\n  <data name=\"Services\" xml:space=\"preserve\">\n    <value>Services</value>\n  </data>\n  <data name=\"SessionLength\" xml:space=\"preserve\">\n    <value>Session length (minutes)</value>\n  </data>\n  <data name=\"Size\" xml:space=\"preserve\">\n    <value>Size</value>\n  </data>\n  <data name=\"SubjectAndBodyNotEmpty\" xml:space=\"preserve\">\n    <value>Subject and body must not be empty</value>\n  </data>\n  <data name=\"Title\" xml:space=\"preserve\">\n    <value>Title</value>\n  </data>\n  <data name=\"TopicsPerPage\" xml:space=\"preserve\">\n    <value>Topics per page</value>\n  </data>\n  <data name=\"Up\" xml:space=\"preserve\">\n    <value>Up</value>\n  </data>\n  <data name=\"UserAvatarMaxDim\" xml:space=\"preserve\">\n    <value>User avatar max dimensions</value>\n  </data>\n  <data name=\"UserDeleteWarning\" xml:space=\"preserve\">\n    <value>WARNNG! Deleting an account is permanent</value>\n  </data>\n  <data name=\"UserImageApproval\" xml:space=\"preserve\">\n    <value>User Image Approval</value>\n  </data>\n  <data name=\"UserImageMaxDim\" xml:space=\"preserve\">\n    <value>User image max dimensions</value>\n  </data>\n  <data name=\"UserRoles\" xml:space=\"preserve\">\n    <value>User Roles</value>\n  </data>\n  <data name=\"Visible\" xml:space=\"preserve\">\n    <value>Visible</value>\n  </data>\n  <data name=\"AddNewForum\" xml:space=\"preserve\">\n    <value>Add a new forum</value>\n  </data>\n  <data name=\"Body\" xml:space=\"preserve\">\n    <value>Body</value>\n  </data>\n  <data name=\"BodyHtml\" xml:space=\"preserve\">\n    <value>HTML Body</value>\n  </data>\n  <data name=\"ClickToLoadMorePosts\" xml:space=\"preserve\">\n    <value>Click to load newer posts before you reply</value>\n  </data>\n  <data name=\"Comment\" xml:space=\"preserve\">\n    <value>Comment</value>\n  </data>\n  <data name=\"CommentsOptional\" xml:space=\"preserve\">\n    <value>Comments (optional)</value>\n  </data>\n  <data name=\"CreateJunkWordButton\" xml:space=\"preserve\">\n    <value>Create New Junk Word</value>\n  </data>\n  <data name=\"CreateNewReply\" xml:space=\"preserve\">\n    <value>Create your reply</value>\n  </data>\n  <data name=\"CreateNewRole\" xml:space=\"preserve\">\n    <value>Create New Role</value>\n  </data>\n  <data name=\"CreateNewTopic\" xml:space=\"preserve\">\n    <value>Create your new topic</value>\n  </data>\n  <data name=\"DeleteAllErrors\" xml:space=\"preserve\">\n    <value>Delete all errors</value>\n  </data>\n  <data name=\"DeleteJunkWordButton\" xml:space=\"preserve\">\n    <value>Delete Selected Junk Word</value>\n  </data>\n  <data name=\"DeleteSelectedRole\" xml:space=\"preserve\">\n    <value>Delete Selected Role</value>\n  </data>\n  <data name=\"EditPost\" xml:space=\"preserve\">\n    <value>Edit Post</value>\n  </data>\n  <data name=\"EmailUsersQueued\" xml:space=\"preserve\">\n    <value>E-mail queued to subscribed users.</value>\n  </data>\n  <data name=\"EndDate\" xml:space=\"preserve\">\n    <value>End date</value>\n  </data>\n  <data name=\"Event\" xml:space=\"preserve\">\n    <value>Event</value>\n  </data>\n  <data name=\"EventTime\" xml:space=\"preserve\">\n    <value>Event Time</value>\n  </data>\n  <data name=\"FavoriteMustBeLoggedIn\" xml:space=\"preserve\">\n    <value>You must be logged in to view favorite topics.</value>\n  </data>\n  <data name=\"FavoritesDontHave\" xml:space=\"preserve\">\n    <value>You don't have any favorite topics.</value>\n  </data>\n  <data name=\"FavoriteTopics\" xml:space=\"preserve\">\n    <value>Favorite Topics</value>\n  </data>\n  <data name=\"ForumMoveError\" xml:space=\"preserve\">\n    <value>There was an unknown error while attempting to move forum</value>\n  </data>\n  <data name=\"ForumPermissionInstructions\" xml:space=\"preserve\">\n    <value>To remove posting or viewing restrictions, remove all roles from the appropriate box.</value>\n  </data>\n  <data name=\"FromEmailAddress\" xml:space=\"preserve\">\n    <value>From e-mail address</value>\n  </data>\n  <data name=\"ID\" xml:space=\"preserve\">\n    <value>ID</value>\n  </data>\n  <data name=\"IncludeSignature\" xml:space=\"preserve\">\n    <value>Include signature</value>\n  </data>\n  <data name=\"IndexingInterval\" xml:space=\"preserve\">\n    <value>Indexing Interval</value>\n  </data>\n  <data name=\"IP\" xml:space=\"preserve\">\n    <value>IP</value>\n  </data>\n  <data name=\"IsRunning\" xml:space=\"preserve\">\n    <value>Is Running</value>\n  </data>\n  <data name=\"JunkWords\" xml:space=\"preserve\">\n    <value>Junk Words</value>\n  </data>\n  <data name=\"MarkForumRead\" xml:space=\"preserve\">\n    <value>Mark forum read</value>\n  </data>\n  <data name=\"Message\" xml:space=\"preserve\">\n    <value>Message</value>\n  </data>\n  <data name=\"Millseconds\" xml:space=\"preserve\">\n    <value>Milliseconds</value>\n  </data>\n  <data name=\"NamePosts\" xml:space=\"preserve\">\n    <value>{0}'s posts</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Optional\" xml:space=\"preserve\">\n    <value>(optional)</value>\n  </data>\n  <data name=\"PermRoles\" xml:space=\"preserve\">\n    <value>Admin and Moderator are permanent roles, and can't be deleted.</value>\n  </data>\n  <data name=\"PostID\" xml:space=\"preserve\">\n    <value>PostID</value>\n  </data>\n  <data name=\"PostingRoles\" xml:space=\"preserve\">\n    <value>Posting Roles</value>\n  </data>\n  <data name=\"PostNewTopic\" xml:space=\"preserve\">\n    <value>Post a new topic</value>\n  </data>\n  <data name=\"PreviewTopic\" xml:space=\"preserve\">\n    <value>Preview topic</value>\n    <comment>preview the verb, not adjective</comment>\n  </data>\n  <data name=\"Remove\" xml:space=\"preserve\">\n    <value>Remove</value>\n  </data>\n  <data name=\"RemoveAll\" xml:space=\"preserve\">\n    <value>Remove All</value>\n  </data>\n  <data name=\"SendingInterval\" xml:space=\"preserve\">\n    <value>Sending interval</value>\n  </data>\n  <data name=\"SmtpPassword\" xml:space=\"preserve\">\n    <value>SMTP Password</value>\n  </data>\n  <data name=\"SmtpPort\" xml:space=\"preserve\">\n    <value>Port (default is 25)</value>\n  </data>\n  <data name=\"SmtpServer\" xml:space=\"preserve\">\n    <value>SMTP Server</value>\n  </data>\n  <data name=\"SmtpUser\" xml:space=\"preserve\">\n    <value>SMTP User</value>\n  </data>\n  <data name=\"StartDate\" xml:space=\"preserve\">\n    <value>Start date</value>\n  </data>\n  <data name=\"SubmitNewTopic\" xml:space=\"preserve\">\n    <value>Submit new topic</value>\n  </data>\n  <data name=\"SubmitReply\" xml:space=\"preserve\">\n    <value>Submit reply</value>\n  </data>\n  <data name=\"TopicID\" xml:space=\"preserve\">\n    <value>TopicID</value>\n  </data>\n  <data name=\"Type\" xml:space=\"preserve\">\n    <value>Type</value>\n  </data>\n  <data name=\"UseEsmtpCred\" xml:space=\"preserve\">\n    <value>Use ESMTP for credentials</value>\n  </data>\n  <data name=\"UserID\" xml:space=\"preserve\">\n    <value>UserID</value>\n  </data>\n  <data name=\"UserIDTarget\" xml:space=\"preserve\">\n    <value>Target UserID</value>\n  </data>\n  <data name=\"UserImageApprovalNotReq\" xml:space=\"preserve\">\n    <value>User uploaded images currently do not require approval</value>\n  </data>\n  <data name=\"UseSsl\" xml:space=\"preserve\">\n    <value>Use SSL</value>\n  </data>\n  <data name=\"ViewingRoles\" xml:space=\"preserve\">\n    <value>Viewing Roles</value>\n  </data>\n  <data name=\"Close\" xml:space=\"preserve\">\n    <value>Close</value>\n  </data>\n  <data name=\"CloseOnReply\" xml:space=\"preserve\">\n    <value>Close on reply</value>\n  </data>\n  <data name=\"FavoriteMake\" xml:space=\"preserve\">\n    <value>Make Favorite</value>\n  </data>\n  <data name=\"FavoriteRemove\" xml:space=\"preserve\">\n    <value>Remove From Favorites</value>\n  </data>\n  <data name=\"Link\" xml:space=\"preserve\">\n    <value>Link</value>\n  </data>\n  <data name=\"Moderator\" xml:space=\"preserve\">\n    <value>Moderator</value>\n  </data>\n  <data name=\"NameAvatar\" xml:space=\"preserve\">\n    <value>{0}'s avatar</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"NameLastEdit\" xml:space=\"preserve\">\n    <value>Last edited by {0}</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Open\" xml:space=\"preserve\">\n    <value>Open</value>\n  </data>\n  <data name=\"Pin\" xml:space=\"preserve\">\n    <value>Pin</value>\n  </data>\n  <data name=\"PostReply\" xml:space=\"preserve\">\n    <value>Post a reply</value>\n  </data>\n  <data name=\"Quote\" xml:space=\"preserve\">\n    <value>Quote</value>\n  </data>\n  <data name=\"RecentTopics\" xml:space=\"preserve\">\n    <value>Recent Topics</value>\n  </data>\n  <data name=\"Reply\" xml:space=\"preserve\">\n    <value>Reply</value>\n  </data>\n  <data name=\"ShowMorePosts\" xml:space=\"preserve\">\n    <value>Show more posts</value>\n  </data>\n  <data name=\"ShowPreviousPosts\" xml:space=\"preserve\">\n    <value>Show previous posts</value>\n  </data>\n  <data name=\"Subscribe\" xml:space=\"preserve\">\n    <value>Subscribe</value>\n  </data>\n  <data name=\"Undelete\" xml:space=\"preserve\">\n    <value>Undelete</value>\n  </data>\n  <data name=\"Unpin\" xml:space=\"preserve\">\n    <value>Unpin</value>\n  </data>\n  <data name=\"Update\" xml:space=\"preserve\">\n    <value>Update</value>\n  </data>\n  <data name=\"Archive\" xml:space=\"preserve\">\n    <value>Archive</value>\n  </data>\n  <data name=\"NewPM\" xml:space=\"preserve\">\n    <value>New Private Message</value>\n  </data>\n  <data name=\"NoResults\" xml:space=\"preserve\">\n    <value>No results found</value>\n  </data>\n  <data name=\"Send\" xml:space=\"preserve\">\n    <value>Send</value>\n  </data>\n  <data name=\"Unarchive\" xml:space=\"preserve\">\n    <value>Unarchive</value>\n  </data>\n  <data name=\"ViewArchivedMessages\" xml:space=\"preserve\">\n    <value>View archived messages</value>\n  </data>\n  <data name=\"DisplayName\" xml:space=\"preserve\">\n    <value>Display name</value>\n  </data>\n  <data name=\"Error\" xml:space=\"preserve\">\n    <value>Error</value>\n  </data>\n  <data name=\"ErrorSettingUpDb\" xml:space=\"preserve\">\n    <value>There was an error setting up your database</value>\n  </data>\n  <data name=\"ForumReady\" xml:space=\"preserve\">\n    <value>Your forum is ready.</value>\n  </data>\n  <data name=\"ForumTitle\" xml:space=\"preserve\">\n    <value>Forum title</value>\n  </data>\n  <data name=\"GoAdmin\" xml:space=\"preserve\">\n    <value>Go to the admin section to finish setting up your Forum.</value>\n  </data>\n  <data name=\"InvalidLink\" xml:space=\"preserve\">\n    <value>That link is no longer valid.</value>\n  </data>\n  <data name=\"NoDataConnection\" xml:space=\"preserve\">\n    <value>No Data Connection</value>\n  </data>\n  <data name=\"PopForumsSetup\" xml:space=\"preserve\">\n    <value>POP Forums Setup</value>\n  </data>\n  <data name=\"SetupCantConnect\" xml:space=\"preserve\">\n    <value>The setup can't connect to the data store. Please check your connection string and configuration.</value>\n  </data>\n  <data name=\"SetupConnSuccess\" xml:space=\"preserve\">\n    <value>A database connection has been successful. Please fill out the following values to get started</value>\n  </data>\n  <data name=\"SetupDatabase\" xml:space=\"preserve\">\n    <value>Setup the database</value>\n  </data>\n  <data name=\"SetupFirstUser\" xml:space=\"preserve\">\n    <value>Setup your first user (will be granted admin and moderator rights)</value>\n  </data>\n  <data name=\"SubscribedTopics\" xml:space=\"preserve\">\n    <value>Subscribed Topics</value>\n  </data>\n  <data name=\"SubscribeLoggedIn\" xml:space=\"preserve\">\n    <value>You must be logged in to view subscribed topics.</value>\n  </data>\n  <data name=\"SubscribeNone\" xml:space=\"preserve\">\n    <value>You aren't subscribed to any topics.</value>\n  </data>\n  <data name=\"Success\" xml:space=\"preserve\">\n    <value>Success!</value>\n  </data>\n  <data name=\"UnsubscribeTopic\" xml:space=\"preserve\">\n    <value>{0} is unsubscribed from {1}</value>\n    <comment>{0} is a name, {1} is a topic title</comment>\n  </data>\n  <data name=\"ArchivedNewPosts\" xml:space=\"preserve\">\n    <value>Archived with new posts</value>\n  </data>\n  <data name=\"Closed\" xml:space=\"preserve\">\n    <value>Closed topic</value>\n  </data>\n  <data name=\"ClosedPinned\" xml:space=\"preserve\">\n    <value>Closed, pinned topic</value>\n  </data>\n  <data name=\"DeleteTopic\" xml:space=\"preserve\">\n    <value>Delete Topic</value>\n  </data>\n  <data name=\"First\" xml:space=\"preserve\">\n    <value>First</value>\n  </data>\n  <data name=\"ForgotPasswordEmail\" xml:space=\"preserve\">\n    <value>To reset your password on {0}, please follow this link:{3}{1}{3}{3}{2}</value>\n    <comment>{0}=title of forum, {1}=email link, {2}=signature, {3}=new line</comment>\n  </data>\n  <data name=\"ForgotPasswordSubject\" xml:space=\"preserve\">\n    <value>Your password reset instructions for {0}</value>\n    <comment>{0} is name of forum</comment>\n  </data>\n  <data name=\"ForumNoPost\" xml:space=\"preserve\">\n    <value>You aren't allowed to post in this forum</value>\n  </data>\n  <data name=\"ForumNoView\" xml:space=\"preserve\">\n    <value>You aren't allowed to view this forum</value>\n  </data>\n  <data name=\"LessThanMinute\" xml:space=\"preserve\">\n    <value>Less than a minute ago</value>\n  </data>\n  <data name=\"LoginToPost\" xml:space=\"preserve\">\n    <value>You must be logged in to post</value>\n  </data>\n  <data name=\"MinutesAgo\" xml:space=\"preserve\">\n    <value>{0} minutes ago</value>\n    <comment>{0} is a number</comment>\n  </data>\n  <data name=\"More\" xml:space=\"preserve\">\n    <value>More</value>\n  </data>\n  <data name=\"NewPosts\" xml:space=\"preserve\">\n    <value>New posts</value>\n  </data>\n  <data name=\"NewPostsClosed\" xml:space=\"preserve\">\n    <value>New posts, closed topic</value>\n  </data>\n  <data name=\"NewPostsClosedPinned\" xml:space=\"preserve\">\n    <value>New posts, closed &amp; pinned topic</value>\n  </data>\n  <data name=\"NewPostsPinned\" xml:space=\"preserve\">\n    <value>New posts, pinned topic</value>\n  </data>\n  <data name=\"Next\" xml:space=\"preserve\">\n    <value>Next</value>\n  </data>\n  <data name=\"NoNewPosts\" xml:space=\"preserve\">\n    <value>No new posts</value>\n  </data>\n  <data name=\"NotLoggedIn\" xml:space=\"preserve\">\n    <value>You are not logged in</value>\n  </data>\n  <data name=\"OneMinuteAgo\" xml:space=\"preserve\">\n    <value>1 minute ago</value>\n  </data>\n  <data name=\"Pinned\" xml:space=\"preserve\">\n    <value>Pinned topic</value>\n  </data>\n  <data name=\"PMCreateWarnings\" xml:space=\"preserve\">\n\t  <value>Must include message and at least one user</value>\n  </data>\n  <data name=\"PostEmpty\" xml:space=\"preserve\">\n    <value>Can't make an empty post</value>\n  </data>\n  <data name=\"PostWait\" xml:space=\"preserve\">\n    <value>You must wait {0} seconds between posts and may not duplicate your last post</value>\n    <comment>{0} is a number for seconds</comment>\n  </data>\n  <data name=\"Previous\" xml:space=\"preserve\">\n    <value>Previous</value>\n  </data>\n  <data name=\"RegisterEmailSubject\" xml:space=\"preserve\">\n    <value>Forum registration - {0}</value>\n    <comment>{0} is name of forum</comment>\n  </data>\n  <data name=\"RegisterEmailThankYou\" xml:space=\"preserve\">\n    <value>Thank you for registering with {0}.{2}{2}{1}</value>\n    <comment>{0}=name of forum, {1}=forum link, {2}=new line</comment>\n  </data>\n  <data name=\"RegisterEmailThankYouVerify\" xml:space=\"preserve\">\n    <value>Thank you for registering with {0}. Please follow this link to confirm your registration:{5}{5}{1} {5}{5}If you can't follow this link, please visit the following page, and cut and paste the confirmation code:{5}{5}Link: {2} {5}Code: {3} {5}{5}{4}</value>\n    <comment>{0}=name of forum, {1}=auth link, {2}=verify link, {3}=auth code, {4}=sig, {5}new line</comment>\n  </data>\n  <data name=\"SettingsSaved\" xml:space=\"preserve\">\n    <value>Settings saved</value>\n  </data>\n  <data name=\"TodayTime\" xml:space=\"preserve\">\n    <value>Today, {0}</value>\n    <comment>{0} is a time like 1:43pm</comment>\n  </data>\n  <data name=\"TopicNotExist\" xml:space=\"preserve\">\n    <value>That topic doesn't exist</value>\n  </data>\n  <data name=\"YesterdayTime\" xml:space=\"preserve\">\n    <value>Yesterday, {0}</value>\n    <comment>{0} a time like 1:43pm</comment>\n  </data>\n  <data name=\"Loading\" xml:space=\"preserve\">\n    <value>Loading</value>\n  </data>\n  <data name=\"Voted\" xml:space=\"preserve\">\n    <value>Voted</value>\n  </data>\n  <data name=\"VoteUpPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; voted for a post in the topic: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"ScoringGame\" xml:space=\"preserve\">\n    <value>Scoring game</value>\n  </data>\n  <data name=\"Awards\" xml:space=\"preserve\">\n    <value>Awards</value>\n  </data>\n  <data name=\"EventDefinitions\" xml:space=\"preserve\">\n    <value>Event Definitions</value>\n  </data>\n  <data name=\"AwardDefinitions\" xml:space=\"preserve\">\n    <value>Award Definitions</value>\n  </data>\n  <data name=\"ManualEvent\" xml:space=\"preserve\">\n    <value>Manual Event</value>\n  </data>\n  <data name=\"None\" xml:space=\"preserve\">\n    <value>None</value>\n  </data>\n  <data name=\"ActivityFeed\" xml:space=\"preserve\">\n    <value>Activity Feed</value>\n  </data>\n  <data name=\"NewReplyPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; made a post in the topic: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"NewPostPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; started a new topic: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to topic{3} is topic name</comment>\n  </data>\n  <data name=\"ExternalLogins\" xml:space=\"preserve\">\n    <value>External Logins</value>\n  </data>\n  <data name=\"UseExistingForumAccount\" xml:space=\"preserve\">\n    <value>Use existing forum account</value>\n  </data>\n  <data name=\"ExpiredLogin\" xml:space=\"preserve\">\n    <value>That login has expired or is not valid</value>\n  </data>\n  <data name=\"ExternalLoginsDisabled\" xml:space=\"preserve\">\n    <value>External logins are disabled</value>\n  </data>\n  <data name=\"NoExternalLoginsRegistered\" xml:space=\"preserve\">\n    <value>You have no external logins registered</value>\n  </data>\n  <data name=\"DeletePermanently\" xml:space=\"preserve\">\n    <value>Delete Permanently</value>\n  </data>\n  <data name=\"Preview\" xml:space=\"preserve\">\n    <value>Preview</value>\n  </data>\n  <data name=\"AskAQuestion\" xml:space=\"preserve\">\n    <value>Ask A Question</value>\n  </data>\n  <data name=\"Answers\" xml:space=\"preserve\">\n    <value>Answers</value>\n  </data>\n  <data name=\"QuestionAnswered\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; chose an answer for the question: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"ChooseAnswer\" xml:space=\"preserve\">\n    <value>Choose answer</value>\n  </data>\n  <data name=\"PostAnswer\" xml:space=\"preserve\">\n    <value>Post answer</value>\n  </data>\n  <data name=\"SubmitAnswer\" xml:space=\"preserve\">\n    <value>Submit answer</value>\n  </data>\n  <data name=\"AppRestartRequired\" xml:space=\"preserve\">\n    <value>App restart is required</value>\n  </data>\n  <data name=\"SearchError\" xml:space=\"preserve\">\n    <value>There was a search error</value>\n  </data>\n  <data name=\"BotError\" xml:space=\"preserve\">\n    <value>You might be a robot</value>\n  </data>\n  <data name=\"CloseOldTopics\" xml:space=\"preserve\">\n    <value>Close old topics</value>\n  </data>\n  <data name=\"Days\" xml:space=\"preserve\">\n    <value>days</value>\n  </data>\n  <data name=\"PrivateForum\" xml:space=\"preserve\">\n    <value>Private forum</value>\n  </data>\n  <data name=\"AccountNotVerified\" xml:space=\"preserve\">\n    <value>Account is not verified</value>\n  </data>\n  <data name=\"SelectText\" xml:space=\"preserve\">\n\t  <value>Select Text</value>\n  </data>\n  <data name=\"Notifications\" xml:space=\"preserve\">\n\t  <value>Notifications</value>\n  </data>\n  <data name=\"NewReplyNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; made a post in the topic: &lt;b&gt;{1}&lt;/b&gt;\n</value>\n  </data>\n  <data name=\"Award\" xml:space=\"preserve\">\n\t  <value>Award</value>\n  </data>\n  <data name=\"VoteUpNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; voted for a post in the topic: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"QuestionAnsweredNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; chose an answer for the question: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"AutoFollow\" xml:space=\"preserve\">\n\t  <value>Automatically follow topics that you reply to</value>\n  </data>\n  <data name=\"MarkAllRead\" xml:space=\"preserve\">\n\t  <value>Mark all read</value>\n  </data>\n  <data name=\"UploadImage\" xml:space=\"preserve\">\n\t  <value>Upload Image</value>\n  </data>\n  <data name=\"Ignored\" xml:space=\"preserve\">\n\t  <value>Ignored</value>\n  </data>\n  <data name=\"Ignore\" xml:space=\"preserve\">\n\t  <value>Ignore</value>\n  </data>\n  <data name=\"IgnoreList\" xml:space=\"preserve\">\n\t  <value>Ignore List</value>\n  </data>\n</root>"
  },
  {
    "path": "src/PopForums/Resources/Resources.uk.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"Account\" xml:space=\"preserve\">\n    <value>Акаунт</value>\n  </data>\n  <data name=\"AccountCreated\" xml:space=\"preserve\">\n    <value>Акаунт створений</value>\n  </data>\n  <data name=\"AccountReady\" xml:space=\"preserve\">\n    <value>Ваш акаунт був створений, і ви можете почати роботу з форумом.</value>\n  </data>\n  <data name=\"AccountReadyCheckEmail\" xml:space=\"preserve\">\n    <value>Будь ласка перевірте вашу електронну скриньку на наявність підтверджуючого повідомлення для початку користування форумом.</value>\n  </data>\n  <data name=\"AccountVerified\" xml:space=\"preserve\">\n    <value>Ваш акаунт був підтверджений</value>\n  </data>\n  <data name=\"AlreadyCreatedAccount\" xml:space=\"preserve\">\n    <value>Ви вже є залоговані і маєте акаунт.</value>\n  </data>\n  <data name=\"AsYouWouldLikeItToAppear\" xml:space=\"preserve\">\n    <value>(Як би ви хотіли, щоб він з'явився)</value>\n  </data>\n  <data name=\"AtLeastSixChar\" xml:space=\"preserve\">\n    <value>(як мінімум 6 символів)</value>\n  </data>\n  <data name=\"By\" xml:space=\"preserve\">\n    <value />\n    <comment>context: \"Last post: 12am by Jeff\"</comment>\n  </data>\n  <data name=\"CreateAccountButton\" xml:space=\"preserve\">\n    <value>Створити Акаунт</value>\n  </data>\n  <data name=\"CreateAnAccount\" xml:space=\"preserve\">\n    <value>Створити акаунт</value>\n  </data>\n  <data name=\"EditYourProfile\" xml:space=\"preserve\">\n    <value>Редагувати ваш профіль</value>\n  </data>\n  <data name=\"Email\" xml:space=\"preserve\">\n    <value>E-mail</value>\n  </data>\n  <data name=\"EmailProblemAccount\" xml:space=\"preserve\">\n    <value>Відбулась проблема з надсиланням листа з перевіркою електронної пошти:</value>\n  </data>\n  <data name=\"Favorites\" xml:space=\"preserve\">\n    <value>Улюблені</value>\n  </data>\n  <data name=\"Forbidden\" xml:space=\"preserve\">\n    <value>На жаль, у вас немає дозволу для перегляду цього.</value>\n  </data>\n  <data name=\"Forums\" xml:space=\"preserve\">\n    <value>Форуми</value>\n  </data>\n  <data name=\"HaveReadTOS\" xml:space=\"preserve\">\n    <value>Я прочитав і згоден з умовами надання послуг</value>\n  </data>\n  <data name=\"Last\" xml:space=\"preserve\">\n    <value>Останній</value>\n  </data>\n  <data name=\"Login\" xml:space=\"preserve\">\n    <value>Вхід</value>\n  </data>\n  <data name=\"Logout\" xml:space=\"preserve\">\n    <value>Вихід</value>\n  </data>\n  <data name=\"MarkAllForumsRead\" xml:space=\"preserve\">\n    <value>Позначити всі форуми як прочитані</value>\n  </data>\n  <data name=\"MyBirthdayIsOnOrBefore\" xml:space=\"preserve\">\n    <value>Мій день народження {0}</value>\n    <comment>where {0} is a date</comment>\n  </data>\n  <data name=\"Name\" xml:space=\"preserve\">\n    <value>Ім'я</value>\n  </data>\n  <data name=\"NeedToVerifyExistingAccount\" xml:space=\"preserve\">\n    <value>Потрібно підтвердити існуючий акаунт?</value>\n  </data>\n  <data name=\"PageNotFound\" xml:space=\"preserve\">\n    <value>Сторінку не знайдено</value>\n  </data>\n  <data name=\"Password\" xml:space=\"preserve\">\n    <value>Пароль</value>\n  </data>\n  <data name=\"Posts\" xml:space=\"preserve\">\n    <value>Повідомлення</value>\n  </data>\n  <data name=\"PrivateMessages\" xml:space=\"preserve\">\n    <value>Приватні повідомлення</value>\n  </data>\n  <data name=\"Recent\" xml:space=\"preserve\">\n    <value>Останні</value>\n  </data>\n  <data name=\"RegisteredUsers\" xml:space=\"preserve\">\n    <value>Зареєстровані користувачі</value>\n  </data>\n  <data name=\"RetypePassword\" xml:space=\"preserve\">\n    <value>Повторіть пароль</value>\n  </data>\n  <data name=\"Search\" xml:space=\"preserve\">\n    <value>Пошук</value>\n  </data>\n  <data name=\"SubscribeToList\" xml:space=\"preserve\">\n    <value>Підписатися на розсилку</value>\n  </data>\n  <data name=\"Subscriptions\" xml:space=\"preserve\">\n    <value>Підписки</value>\n  </data>\n  <data name=\"TermsOfService\" xml:space=\"preserve\">\n    <value>Загальні положення та умови</value>\n  </data>\n  <data name=\"TimeZone\" xml:space=\"preserve\">\n    <value>Часова зона</value>\n  </data>\n  <data name=\"Topics\" xml:space=\"preserve\">\n    <value>Теми</value>\n  </data>\n  <data name=\"Total\" xml:space=\"preserve\">\n    <value>Сума</value>\n  </data>\n  <data name=\"TotalPosts\" xml:space=\"preserve\">\n    <value>Загальна кількість повідомлень</value>\n  </data>\n  <data name=\"TotalTopics\" xml:space=\"preserve\">\n    <value>Загальна кількість тем</value>\n  </data>\n  <data name=\"UseDaylight\" xml:space=\"preserve\">\n    <value>Використовувати N.America/Europe Daylight Saving</value>\n  </data>\n  <data name=\"UsersOnline\" xml:space=\"preserve\">\n    <value>Користувачі онлайн</value>\n  </data>\n  <data name=\"MustBe13\" xml:space=\"preserve\">\n    <value>Ви повинні бути 13-річним або старше, щоб зареєструватися</value>\n  </data>\n  <data name=\"MustAcceptTOS\" xml:space=\"preserve\">\n    <value>Ви повинні прийняти умови надання послуг</value>\n  </data>\n  <data name=\"RetypeYourPassword\" xml:space=\"preserve\">\n    <value>Введіть пароль ще раз</value>\n  </data>\n  <data name=\"NameRequired\" xml:space=\"preserve\">\n    <value>Ім'я потрібно</value>\n  </data>\n  <data name=\"NameInUse\" xml:space=\"preserve\">\n    <value>Дане ім'я вже використовується</value>\n  </data>\n  <data name=\"EmailRequired\" xml:space=\"preserve\">\n    <value>E-mail потрібний</value>\n  </data>\n  <data name=\"ValidEmailAddressRequired\" xml:space=\"preserve\">\n    <value>Дійсна адреса електронної пошти потрібна</value>\n  </data>\n  <data name=\"EmailInUse\" xml:space=\"preserve\">\n    <value>Дана електронна скринька вже використовується</value>\n  </data>\n  <data name=\"EmailBanned\" xml:space=\"preserve\">\n    <value>Даний e-mail був забанений</value>\n  </data>\n  <data name=\"IPBanned\" xml:space=\"preserve\">\n    <value>Ваша IP адреса була забанена</value>\n  </data>\n  <data name=\"RetypePasswordMustMatch\" xml:space=\"preserve\">\n    <value>Введені паролі не співпадають</value>\n  </data>\n  <data name=\"EditAccount\" xml:space=\"preserve\">\n    <value>Редагувати Акаунт</value>\n  </data>\n  <data name=\"EditProfile\" xml:space=\"preserve\">\n    <value>Редагувати Профіль</value>\n  </data>\n  <data name=\"EnterVerificationCode\" xml:space=\"preserve\">\n    <value>Будь ласка, введіть код підтвердження</value>\n  </data>\n  <data name=\"ForumHomePage\" xml:space=\"preserve\">\n    <value>Домашня сторінка форуму</value>\n  </data>\n  <data name=\"MustBeRegisteredToEditAccount\" xml:space=\"preserve\">\n    <value>Ви повинні бути зареєстровані та залоговані щоб редагувати ваш акаунт.</value>\n  </data>\n  <data name=\"NoUserFoundWithEmail\" xml:space=\"preserve\">\n    <value>Не знайдено користувача з таким e-mail</value>\n  </data>\n  <data name=\"SendEmailWithNewCodeButton\" xml:space=\"preserve\">\n    <value>Надіслати E-mail З Новим Кодом</value>\n  </data>\n  <data name=\"VerificationEmailSent\" xml:space=\"preserve\">\n    <value>Підтвердження було надіслане на вашу скриньку</value>\n  </data>\n  <data name=\"VerificationFailure\" xml:space=\"preserve\">\n    <value>Підтвердження провалено</value>\n  </data>\n  <data name=\"VerificationIfYouNeed\" xml:space=\"preserve\">\n    <value>Якщо вам потрібен новий код підтвердження, введіть свою адресу електронної пошти тут</value>\n  </data>\n  <data name=\"VerificationLinkBad\" xml:space=\"preserve\">\n    <value>Вибачте, підтверджуюче посилання, за яким ви пробуєте перейти не є коректне.</value>\n  </data>\n  <data name=\"VerifyAccount\" xml:space=\"preserve\">\n    <value>Підтвердити Акаунт</value>\n  </data>\n  <data name=\"VerifyCodeButton\" xml:space=\"preserve\">\n    <value>Підтвердити Код</value>\n  </data>\n  <data name=\"Avatar\" xml:space=\"preserve\">\n    <value>Аватарка</value>\n  </data>\n  <data name=\"AvatarDelete\" xml:space=\"preserve\">\n    <value>Видалити цей аватар</value>\n  </data>\n  <data name=\"ChangeEmail\" xml:space=\"preserve\">\n    <value>Змінити E-mail</value>\n  </data>\n  <data name=\"ChangeEmailButton\" xml:space=\"preserve\">\n    <value>Змінити e-mail і надіслати код</value>\n  </data>\n  <data name=\"ChangeEmailConsequence\" xml:space=\"preserve\">\n    <value>Зміна вашої електронної пошти викличе генерацію нового коду підтвердження, який буде надісланий на нову адресу. Ви не будете мати змогу залишати повідомлення, поки ви не підтвердите вашу електронну адресу з новим кодом.</value>\n  </data>\n  <data name=\"ChangePassword\" xml:space=\"preserve\">\n    <value>Змінити Пароль</value>\n  </data>\n  <data name=\"ChangeYourEmailPassword\" xml:space=\"preserve\">\n    <value>Змінити вашу електронну скриньку або пароль</value>\n  </data>\n  <data name=\"DateOfBirth\" xml:space=\"preserve\">\n    <value>Дата Народження</value>\n  </data>\n  <data name=\"Details\" xml:space=\"preserve\">\n    <value>Деталі</value>\n  </data>\n  <data name=\"EmailChangeSuccess\" xml:space=\"preserve\">\n    <value>Ваш e-mail був успішно змінений</value>\n  </data>\n  <data name=\"EmailNew\" xml:space=\"preserve\">\n    <value>Нова e-mail адреса</value>\n  </data>\n  <data name=\"EmailNewRetype\" xml:space=\"preserve\">\n    <value>Повторіть нову e-mail адресу</value>\n  </data>\n  <data name=\"EmailNotFound\" xml:space=\"preserve\">\n    <value>E-mail не було знайдено</value>\n  </data>\n  <data name=\"EmailSent\" xml:space=\"preserve\">\n    <value>Ваш e-mail був відправлений</value>\n  </data>\n  <data name=\"EmailsMustMatch\" xml:space=\"preserve\">\n    <value>Дані e-mail адреси не співпадають</value>\n  </data>\n  <data name=\"EmailUser\" xml:space=\"preserve\">\n    <value>E-mail Користувача</value>\n  </data>\n  <data name=\"ForcePlainTextBox\" xml:space=\"preserve\">\n    <value>Прості текстові поля</value>\n  </data>\n  <data name=\"ForgotInstructions\" xml:space=\"preserve\">\n    <value>Введіть адресу електронної пошти, пов'язану з вашим аккаунтом, щоб отримати інструкції по зміні пароля:</value>\n  </data>\n  <data name=\"ForgotInstructionsSent\" xml:space=\"preserve\">\n    <value>Інструкції були надіслані на електронну скриньку</value>\n  </data>\n  <data name=\"ForgotPassword\" xml:space=\"preserve\">\n    <value>Забули Пароль</value>\n  </data>\n  <data name=\"ForgotPasswordQuestion\" xml:space=\"preserve\">\n    <value>Забули пароль?</value>\n  </data>\n  <data name=\"HideVanity\" xml:space=\"preserve\">\n    <value>Hide vanity</value>\n  </data>\n  <data name=\"In\" xml:space=\"preserve\">\n    <value>в</value>\n    <comment>context: started by Jeff in General Forum</comment>\n  </data>\n  <data name=\"Joined\" xml:space=\"preserve\">\n    <value>Зареєструвався</value>\n  </data>\n  <data name=\"Location\" xml:space=\"preserve\">\n    <value>Місцезнаходження</value>\n  </data>\n  <data name=\"LoginAlready\" xml:space=\"preserve\">\n    <value>Ви вже залоговані.</value>\n  </data>\n  <data name=\"LoginBad\" xml:space=\"preserve\">\n    <value>Погана скринька або пароль</value>\n  </data>\n  <data name=\"ManagePhotos\" xml:space=\"preserve\">\n    <value>Керування фотографіями</value>\n  </data>\n  <data name=\"ManageYourPhotos\" xml:space=\"preserve\">\n    <value>Керування вашими фотографіями</value>\n  </data>\n  <data name=\"NewPasswordSaved\" xml:space=\"preserve\">\n    <value>Новий пароль збережено</value>\n  </data>\n  <data name=\"NotRegisteredQuestion\" xml:space=\"preserve\">\n    <value>Не зареєстровані?</value>\n  </data>\n  <data name=\"OldPasswordIncorrect\" xml:space=\"preserve\">\n    <value>Старий пароль невірний</value>\n  </data>\n  <data name=\"Options\" xml:space=\"preserve\">\n    <value>Опції</value>\n  </data>\n  <data name=\"PasswordNew\" xml:space=\"preserve\">\n    <value>Новий пароль</value>\n  </data>\n  <data name=\"PasswordNewRetype\" xml:space=\"preserve\">\n    <value>Підтвердіть ваш новий пароль</value>\n  </data>\n  <data name=\"PasswordOld\" xml:space=\"preserve\">\n    <value>Старий пароль</value>\n  </data>\n  <data name=\"PasswordReset\" xml:space=\"preserve\">\n    <value>Скинути Пароль</value>\n  </data>\n  <data name=\"PasswordResetLinkInvalid\" xml:space=\"preserve\">\n    <value>Дане посилання на скидання паролю не є коректне.</value>\n  </data>\n  <data name=\"PasswordResetNote\" xml:space=\"preserve\">\n    <value>Ваш новий пароль активний і ви тепер увійшли в систему</value>\n  </data>\n  <data name=\"PasswordResetSuccess\" xml:space=\"preserve\">\n    <value>Скидання Паролю Пройшло Успішно</value>\n  </data>\n  <data name=\"Photo\" xml:space=\"preserve\">\n    <value>Фото</value>\n  </data>\n  <data name=\"PhotoDelete\" xml:space=\"preserve\">\n    <value>Видалити це фото</value>\n  </data>\n  <data name=\"PhotoNotApproved\" xml:space=\"preserve\">\n    <value>(дана картинка не була підтверджена адміністратором)</value>\n  </data>\n  <data name=\"ProfileUpdated\" xml:space=\"preserve\">\n    <value>Профіль збережено</value>\n  </data>\n  <data name=\"RememberMe\" xml:space=\"preserve\">\n    <value>Запамятати мене?</value>\n  </data>\n  <data name=\"Replies\" xml:space=\"preserve\">\n    <value>Відповіді</value>\n  </data>\n  <data name=\"Save\" xml:space=\"preserve\">\n    <value>Зберегти</value>\n  </data>\n  <data name=\"SendEmailButton\" xml:space=\"preserve\">\n    <value>Надіслати E-mail</value>\n  </data>\n  <data name=\"SendPM\" xml:space=\"preserve\">\n    <value>Надіслати Приватне Повідомлення</value>\n  </data>\n  <data name=\"ShowProfileDetails\" xml:space=\"preserve\">\n    <value>Показати деталі вашого профілю (але не e-mail)</value>\n  </data>\n  <data name=\"Signature\" xml:space=\"preserve\">\n    <value>Підпис</value>\n  </data>\n  <data name=\"StartedBy\" xml:space=\"preserve\">\n    <value>Автор</value>\n  </data>\n  <data name=\"Subject\" xml:space=\"preserve\">\n    <value>Тема</value>\n  </data>\n  <data name=\"Text\" xml:space=\"preserve\">\n    <value>Текст</value>\n  </data>\n  <data name=\"To\" xml:space=\"preserve\">\n    <value>До</value>\n  </data>\n  <data name=\"UploadNew\" xml:space=\"preserve\">\n    <value>Завантажити нову</value>\n  </data>\n  <data name=\"UserNotFound\" xml:space=\"preserve\">\n    <value>Користувача не знайдено</value>\n  </data>\n  <data name=\"Views\" xml:space=\"preserve\">\n    <value>Переглядів</value>\n  </data>\n  <data name=\"Web\" xml:space=\"preserve\">\n    <value>Веб</value>\n  </data>\n  <data name=\"WebVisit\" xml:space=\"preserve\">\n    <value>Відвідати сайт користувача</value>\n  </data>\n  <data name=\"YourIP\" xml:space=\"preserve\">\n    <value>Ваша IP адреса</value>\n  </data>\n  <data name=\"Birthday\" xml:space=\"preserve\">\n    <value>День Народження</value>\n  </data>\n  <data name=\"Contact\" xml:space=\"preserve\">\n    <value>Контакт</value>\n  </data>\n  <data name=\"Profile\" xml:space=\"preserve\">\n    <value>Профіль</value>\n  </data>\n  <data name=\"SendNameEmail\" xml:space=\"preserve\">\n    <value>Надіслати {0} an e-mail message</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"SendNamePM\" xml:space=\"preserve\">\n    <value>Надіслати {0} приватне повідомлення</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Unsubscribe\" xml:space=\"preserve\">\n    <value>Відписатись</value>\n  </data>\n  <data name=\"UnsubscribeFail\" xml:space=\"preserve\">\n    <value>Відписування від теми невдалось</value>\n  </data>\n  <data name=\"UnsubscribeLinkBad\" xml:space=\"preserve\">\n    <value>Дане посилання не є правильним.</value>\n  </data>\n  <data name=\"UnsubscribeNote\" xml:space=\"preserve\">\n    <value>Ваш запит на відписування прийнято для обробки. Ви можете підписатись знову,залогувавшись і перейшовши на сторінку вашого акаунта.</value>\n  </data>\n  <data name=\"AddForum\" xml:space=\"preserve\">\n    <value>Додати Форум</value>\n  </data>\n  <data name=\"AddNew\" xml:space=\"preserve\">\n    <value>Додати Новий</value>\n  </data>\n  <data name=\"AllowImages\" xml:space=\"preserve\">\n    <value>Дозволити зображення</value>\n  </data>\n  <data name=\"Archived\" xml:space=\"preserve\">\n    <value>В архіві</value>\n  </data>\n  <data name=\"AreYouSure\" xml:space=\"preserve\">\n    <value>Ви впевнені?</value>\n  </data>\n  <data name=\"Ban\" xml:space=\"preserve\">\n    <value>Забанити</value>\n  </data>\n  <data name=\"BanRemove\" xml:space=\"preserve\">\n    <value>Розбанити</value>\n  </data>\n  <data name=\"Categories\" xml:space=\"preserve\">\n    <value>Категорії</value>\n  </data>\n  <data name=\"Category\" xml:space=\"preserve\">\n    <value>Категорія</value>\n  </data>\n  <data name=\"CategoryEditTitle\" xml:space=\"preserve\">\n    <value>Редагувати Заголовок Категорії</value>\n  </data>\n  <data name=\"CategoryMoveError\" xml:space=\"preserve\">\n    <value>Була невідома помилка при спробі перемістити категорію</value>\n  </data>\n  <data name=\"CensorReplacementChar\" xml:space=\"preserve\">\n    <value>Символи заміни цензури</value>\n  </data>\n  <data name=\"CensorWords\" xml:space=\"preserve\">\n    <value>Цензурні слова</value>\n  </data>\n  <data name=\"Delete\" xml:space=\"preserve\">\n    <value>Видалити</value>\n  </data>\n  <data name=\"DeleteAndBanButton\" xml:space=\"preserve\">\n    <value>Видалити і Забанити Користувача</value>\n  </data>\n  <data name=\"DeleteUserButton\" xml:space=\"preserve\">\n    <value>Видалити Користувача</value>\n  </data>\n  <data name=\"Description\" xml:space=\"preserve\">\n    <value>Опис</value>\n  </data>\n  <data name=\"Down\" xml:space=\"preserve\">\n    <value>Вниз</value>\n  </data>\n  <data name=\"Edit\" xml:space=\"preserve\">\n    <value>Редагувати</value>\n  </data>\n  <data name=\"EditForum\" xml:space=\"preserve\">\n    <value>Редагувати Форум</value>\n  </data>\n  <data name=\"EditUser\" xml:space=\"preserve\">\n    <value>Редагувати Користувача</value>\n  </data>\n  <data name=\"EmailBan\" xml:space=\"preserve\">\n    <value>Забанити E-mail</value>\n  </data>\n  <data name=\"EmailIpBan\" xml:space=\"preserve\">\n    <value>E-mail/IP Бан</value>\n  </data>\n  <data name=\"EmailNewOptional\" xml:space=\"preserve\">\n    <value>Новий e-mail (бажано)</value>\n  </data>\n  <data name=\"EmailUsers\" xml:space=\"preserve\">\n    <value>E-mail Користувачів</value>\n  </data>\n  <data name=\"ErrorLog\" xml:space=\"preserve\">\n    <value>ЛогПомилок</value>\n  </data>\n  <data name=\"Forum\" xml:space=\"preserve\">\n    <value>Форум</value>\n  </data>\n  <data name=\"ForumAdapter\" xml:space=\"preserve\">\n    <value>Forum Adapter (optional, use \"Namespace.Type, AssemblyName\")</value>\n  </data>\n  <data name=\"ForumHome\" xml:space=\"preserve\">\n    <value>Домашня Форуму</value>\n  </data>\n  <data name=\"ForumPermisions\" xml:space=\"preserve\">\n    <value>Дозволи форуму</value>\n  </data>\n  <data name=\"ForumSettings\" xml:space=\"preserve\">\n    <value>Налаштування Форуму</value>\n  </data>\n  <data name=\"ForumsUncat\" xml:space=\"preserve\">\n    <value>Форуми без категорій</value>\n  </data>\n  <data name=\"GeneralSettings\" xml:space=\"preserve\">\n    <value>Загальні Налаштування</value>\n  </data>\n  <data name=\"IpBan\" xml:space=\"preserve\">\n    <value>IP Бан</value>\n  </data>\n  <data name=\"IpHistory\" xml:space=\"preserve\">\n    <value>IP Історія</value>\n  </data>\n  <data name=\"IsApproved\" xml:space=\"preserve\">\n    <value>Є підтверджений</value>\n  </data>\n  <data name=\"IsSubscribed\" xml:space=\"preserve\">\n    <value>Є підписаний</value>\n  </data>\n  <data name=\"LogErrors\" xml:space=\"preserve\">\n    <value>Помилки логування</value>\n  </data>\n  <data name=\"Logging\" xml:space=\"preserve\">\n    <value>Логування</value>\n  </data>\n  <data name=\"LogMod\" xml:space=\"preserve\">\n    <value>Лог модерації</value>\n  </data>\n  <data name=\"LogSecurity\" xml:space=\"preserve\">\n    <value>Лог дій безпеки</value>\n  </data>\n  <data name=\"MinimumTimeBetweenPosts\" xml:space=\"preserve\">\n    <value>Мінімальний час між відповідями (секунд)</value>\n  </data>\n  <data name=\"ModerationLog\" xml:space=\"preserve\">\n    <value>Лог Модерації</value>\n  </data>\n  <data name=\"Move\" xml:space=\"preserve\">\n    <value>Перемістити</value>\n  </data>\n  <data name=\"NewUserApprovedWithoutVerification\" xml:space=\"preserve\">\n    <value>Новий користувач затверджений без підтвердження</value>\n  </data>\n  <data name=\"NewUserImageApprovedWithoutMod\" xml:space=\"preserve\">\n    <value>Нове зображення користувача затверджене без модерації</value>\n  </data>\n  <data name=\"Parsing\" xml:space=\"preserve\">\n    <value>Розбір</value>\n  </data>\n  <data name=\"PasswordNewOptional\" xml:space=\"preserve\">\n    <value>Новий пароль (бажано)</value>\n  </data>\n  <data name=\"PopForumsAdmin\" xml:space=\"preserve\">\n    <value>POP Forums Administration</value>\n  </data>\n  <data name=\"PostsPerPage\" xml:space=\"preserve\">\n    <value>Повідомлень на сторінку</value>\n  </data>\n  <data name=\"Role\" xml:space=\"preserve\">\n    <value>Роль</value>\n  </data>\n  <data name=\"Roles\" xml:space=\"preserve\">\n    <value>Ролі</value>\n  </data>\n  <data name=\"Security\" xml:space=\"preserve\">\n    <value>Безпека</value>\n  </data>\n  <data name=\"SecurityLog\" xml:space=\"preserve\">\n    <value>Лог Безпеки</value>\n  </data>\n  <data name=\"ServerTime\" xml:space=\"preserve\">\n    <value>Час Серверу</value>\n  </data>\n  <data name=\"Services\" xml:space=\"preserve\">\n    <value>Послуги</value>\n  </data>\n  <data name=\"SessionLength\" xml:space=\"preserve\">\n    <value>Довжина сесії (хвилини)</value>\n  </data>\n  <data name=\"Size\" xml:space=\"preserve\">\n    <value>Розмір</value>\n  </data>\n  <data name=\"SubjectAndBodyNotEmpty\" xml:space=\"preserve\">\n    <value>Тема і тіло не повинні бути порожніми</value>\n  </data>\n  <data name=\"Title\" xml:space=\"preserve\">\n    <value>Заголовок</value>\n  </data>\n  <data name=\"TopicsPerPage\" xml:space=\"preserve\">\n    <value>Тем на сторінку</value>\n  </data>\n  <data name=\"Up\" xml:space=\"preserve\">\n    <value>Вгору</value>\n  </data>\n  <data name=\"UserAvatarMaxDim\" xml:space=\"preserve\">\n    <value>Максимальний розмір картинки користувача</value>\n  </data>\n  <data name=\"UserDeleteWarning\" xml:space=\"preserve\">\n    <value>УВАГА! Видалення акаунту є необоротнє</value>\n  </data>\n  <data name=\"UserImageApproval\" xml:space=\"preserve\">\n    <value>Підтвердження Картинки Користувача</value>\n  </data>\n  <data name=\"UserImageMaxDim\" xml:space=\"preserve\">\n    <value>Максимальний розмір картинки користувача</value>\n  </data>\n  <data name=\"UserRoles\" xml:space=\"preserve\">\n    <value>Ролі Користувача</value>\n  </data>\n  <data name=\"Visible\" xml:space=\"preserve\">\n    <value>Видимий</value>\n  </data>\n  <data name=\"AddNewForum\" xml:space=\"preserve\">\n    <value>Додати новий форум</value>\n  </data>\n  <data name=\"Body\" xml:space=\"preserve\">\n    <value>Тіло</value>\n  </data>\n  <data name=\"BodyHtml\" xml:space=\"preserve\">\n    <value>HTML Тіло</value>\n  </data>\n  <data name=\"ClickToLoadMorePosts\" xml:space=\"preserve\">\n    <value>Натисніть щоб завантажити нові повідомлення перед вашою відповіддю</value>\n  </data>\n  <data name=\"Comment\" xml:space=\"preserve\">\n    <value>Коментар</value>\n  </data>\n  <data name=\"CommentsOptional\" xml:space=\"preserve\">\n    <value>Коментарі (за бажанням)</value>\n  </data>\n  <data name=\"CreateJunkWordButton\" xml:space=\"preserve\">\n    <value>Створити New Junk Word</value>\n  </data>\n  <data name=\"CreateNewReply\" xml:space=\"preserve\">\n    <value>Створити вашу відповідь</value>\n  </data>\n  <data name=\"CreateNewRole\" xml:space=\"preserve\">\n    <value>Створити Нову Роль</value>\n  </data>\n  <data name=\"CreateNewTopic\" xml:space=\"preserve\">\n    <value>Створити Нову Тему</value>\n  </data>\n  <data name=\"DeleteAllErrors\" xml:space=\"preserve\">\n    <value>Видалити всі помилки</value>\n  </data>\n  <data name=\"DeleteJunkWordButton\" xml:space=\"preserve\">\n    <value>Видалити Вибраний Junk Word</value>\n  </data>\n  <data name=\"DeleteSelectedRole\" xml:space=\"preserve\">\n    <value>Видалити Вибрану Роль</value>\n  </data>\n  <data name=\"EditPost\" xml:space=\"preserve\">\n    <value>Редагувати Повідомлення</value>\n  </data>\n  <data name=\"EmailUsersQueued\" xml:space=\"preserve\">\n    <value>E-mail queued to subscribed users.</value>\n  </data>\n  <data name=\"EndDate\" xml:space=\"preserve\">\n    <value>Кінцева дата</value>\n  </data>\n  <data name=\"Event\" xml:space=\"preserve\">\n    <value>Подія</value>\n  </data>\n  <data name=\"EventTime\" xml:space=\"preserve\">\n    <value>Час Події</value>\n  </data>\n  <data name=\"FavoriteMustBeLoggedIn\" xml:space=\"preserve\">\n    <value>Ви повинні залогуватись щоб переглядати улюблені теми.</value>\n  </data>\n  <data name=\"FavoritesDontHave\" xml:space=\"preserve\">\n    <value>У вас немає улюблених тем.</value>\n  </data>\n  <data name=\"FavoriteTopics\" xml:space=\"preserve\">\n    <value>Улюблені Теми</value>\n  </data>\n  <data name=\"ForumMoveError\" xml:space=\"preserve\">\n    <value>Відбулась невідома помилка при спробі перемістити форум</value>\n  </data>\n  <data name=\"ForumPermissionInstructions\" xml:space=\"preserve\">\n    <value>Для видалення публікації або обмежень перегляду, видаліть всі ролі з відповідної скриньки.</value>\n  </data>\n  <data name=\"FromEmailAddress\" xml:space=\"preserve\">\n    <value>З e-mail адреси</value>\n  </data>\n  <data name=\"ID\" xml:space=\"preserve\">\n    <value>ID</value>\n  </data>\n  <data name=\"IncludeSignature\" xml:space=\"preserve\">\n    <value>Додати підпис</value>\n  </data>\n  <data name=\"IndexingInterval\" xml:space=\"preserve\">\n    <value>Інтервал індексу</value>\n  </data>\n  <data name=\"IP\" xml:space=\"preserve\">\n    <value>IP</value>\n  </data>\n  <data name=\"IsRunning\" xml:space=\"preserve\">\n    <value>Is Running</value>\n  </data>\n  <data name=\"JunkWords\" xml:space=\"preserve\">\n    <value>Junk Words</value>\n  </data>\n  <data name=\"MarkForumRead\" xml:space=\"preserve\">\n    <value>Позначити форум як прочитаний</value>\n  </data>\n  <data name=\"Message\" xml:space=\"preserve\">\n    <value>Повідомлення</value>\n  </data>\n  <data name=\"Millseconds\" xml:space=\"preserve\">\n    <value>Мілісекунд</value>\n  </data>\n  <data name=\"NamePosts\" xml:space=\"preserve\">\n    <value>Повідомлення {0}</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Optional\" xml:space=\"preserve\">\n    <value>(бажано)</value>\n  </data>\n  <data name=\"PermRoles\" xml:space=\"preserve\">\n    <value>Адміністратор і Модератор є постійними ролями, і не можуть бути видалені.</value>\n  </data>\n  <data name=\"PostID\" xml:space=\"preserve\">\n    <value>ID повідомлення </value>\n  </data>\n  <data name=\"PostingRoles\" xml:space=\"preserve\">\n    <value>Posting Roles</value>\n  </data>\n  <data name=\"PostNewTopic\" xml:space=\"preserve\">\n    <value>Створити нову тему</value>\n  </data>\n  <data name=\"PreviewTopic\" xml:space=\"preserve\">\n    <value>Попередній перегляд теми</value>\n    <comment>preview the verb, not adjective</comment>\n  </data>\n  <data name=\"Remove\" xml:space=\"preserve\">\n    <value>Видалити</value>\n  </data>\n  <data name=\"RemoveAll\" xml:space=\"preserve\">\n    <value>Видалити Все</value>\n  </data>\n  <data name=\"SendingInterval\" xml:space=\"preserve\">\n    <value>Інтервал Відправки</value>\n  </data>\n  <data name=\"SmtpPassword\" xml:space=\"preserve\">\n    <value>SMTP Пароль</value>\n  </data>\n  <data name=\"SmtpPort\" xml:space=\"preserve\">\n    <value>Порт (25 є за замовчуванням)</value>\n  </data>\n  <data name=\"SmtpServer\" xml:space=\"preserve\">\n    <value>SMTP Сервер</value>\n  </data>\n  <data name=\"SmtpUser\" xml:space=\"preserve\">\n    <value>SMTP Користувач</value>\n  </data>\n  <data name=\"StartDate\" xml:space=\"preserve\">\n    <value>Дата початку</value>\n  </data>\n  <data name=\"SubmitNewTopic\" xml:space=\"preserve\">\n    <value>Додати нову тему</value>\n  </data>\n  <data name=\"SubmitReply\" xml:space=\"preserve\">\n    <value>Надіслати відповідь</value>\n  </data>\n  <data name=\"TopicID\" xml:space=\"preserve\">\n    <value>ID Теми</value>\n  </data>\n  <data name=\"Type\" xml:space=\"preserve\">\n    <value>Тип</value>\n  </data>\n  <data name=\"UseEsmtpCred\" xml:space=\"preserve\">\n    <value>Використовувати ESMTP for credentials</value>\n  </data>\n  <data name=\"UserID\" xml:space=\"preserve\">\n    <value>ID користувача</value>\n  </data>\n  <data name=\"UserIDTarget\" xml:space=\"preserve\">\n    <value>Target UserID</value>\n  </data>\n  <data name=\"UserImageApprovalNotReq\" xml:space=\"preserve\">\n    <value>Завантажені картинки користувача зараз не потребують схвалення.</value>\n  </data>\n  <data name=\"UseSsl\" xml:space=\"preserve\">\n    <value>Використовувати SSL</value>\n  </data>\n  <data name=\"ViewingRoles\" xml:space=\"preserve\">\n    <value>Перегляд Ролей</value>\n  </data>\n  <data name=\"Close\" xml:space=\"preserve\">\n    <value>Закрити</value>\n  </data>\n  <data name=\"CloseOnReply\" xml:space=\"preserve\">\n    <value>Закрити після відповіді</value>\n  </data>\n  <data name=\"FavoriteMake\" xml:space=\"preserve\">\n    <value>Додати до Улюблених</value>\n  </data>\n  <data name=\"FavoriteRemove\" xml:space=\"preserve\">\n    <value>Видалити з Улюблених</value>\n  </data>\n  <data name=\"Link\" xml:space=\"preserve\">\n    <value>Посилання</value>\n  </data>\n  <data name=\"Moderator\" xml:space=\"preserve\">\n    <value>Модератор</value>\n  </data>\n  <data name=\"NameAvatar\" xml:space=\"preserve\">\n    <value>Аватарка {0}</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"NameLastEdit\" xml:space=\"preserve\">\n    <value>Редаговане {0}</value>\n    <comment>{0} is a name</comment>\n  </data>\n  <data name=\"Open\" xml:space=\"preserve\">\n    <value>Відкрити</value>\n  </data>\n  <data name=\"Pin\" xml:space=\"preserve\">\n    <value>Закріпити</value>\n  </data>\n  <data name=\"PostReply\" xml:space=\"preserve\">\n    <value>Дати відповідь</value>\n  </data>\n  <data name=\"Quote\" xml:space=\"preserve\">\n    <value>Цитата</value>\n  </data>\n  <data name=\"RecentTopics\" xml:space=\"preserve\">\n    <value>Останні теми</value>\n  </data>\n  <data name=\"Reply\" xml:space=\"preserve\">\n    <value>Відповідь</value>\n  </data>\n  <data name=\"ShowMorePosts\" xml:space=\"preserve\">\n    <value>Показати більше повідомлень</value>\n  </data>\n  <data name=\"ShowPreviousPosts\" xml:space=\"preserve\">\n    <value>Показати попередні повідомлення</value>\n  </data>\n  <data name=\"Subscribe\" xml:space=\"preserve\">\n    <value>Підписатись</value>\n  </data>\n  <data name=\"Undelete\" xml:space=\"preserve\">\n    <value>Відновити</value>\n  </data>\n  <data name=\"Unpin\" xml:space=\"preserve\">\n    <value>Відкріпити</value>\n  </data>\n  <data name=\"Update\" xml:space=\"preserve\">\n    <value>Редагувати</value>\n  </data>\n  <data name=\"Archive\" xml:space=\"preserve\">\n    <value>Архів</value>\n  </data>\n  <data name=\"NewPM\" xml:space=\"preserve\">\n    <value>Нове приватне повідомлення</value>\n  </data>\n  <data name=\"NoResults\" xml:space=\"preserve\">\n    <value>Результатів не знайдено</value>\n  </data>\n  <data name=\"Send\" xml:space=\"preserve\">\n    <value>Надіслати</value>\n  </data>\n  <data name=\"Unarchive\" xml:space=\"preserve\">\n    <value>Перенести з архіву</value>\n  </data>\n  <data name=\"ViewArchivedMessages\" xml:space=\"preserve\">\n    <value>Переглянути Повідомлення в Архіві</value>\n  </data>\n  <data name=\"DisplayName\" xml:space=\"preserve\">\n    <value>Показати Ім'я</value>\n  </data>\n  <data name=\"Error\" xml:space=\"preserve\">\n    <value>Помилка</value>\n  </data>\n  <data name=\"ErrorSettingUpDb\" xml:space=\"preserve\">\n    <value>Сталась помилка налаштовування вашої бази даних</value>\n  </data>\n  <data name=\"ForumReady\" xml:space=\"preserve\">\n    <value>Ваш форум готовий до використання.</value>\n  </data>\n  <data name=\"ForumTitle\" xml:space=\"preserve\">\n    <value>Заголовок форуму</value>\n  </data>\n  <data name=\"GoAdmin\" xml:space=\"preserve\">\n    <value>Перейдіть на адмін панель щоб завершити налаштування вашого Форуму.</value>\n  </data>\n  <data name=\"InvalidLink\" xml:space=\"preserve\">\n    <value>Дане посилання вже не є коректним.</value>\n  </data>\n  <data name=\"NoDataConnection\" xml:space=\"preserve\">\n    <value>Немає Підключення До Даних</value>\n  </data>\n  <data name=\"PopForumsSetup\" xml:space=\"preserve\">\n    <value>POP Forums Setup</value>\n  </data>\n  <data name=\"SetupCantConnect\" xml:space=\"preserve\">\n    <value>Програма установки не може підключитися до сховища даних. Будь ласка, перевірте ваш рядок підключення та налаштування.</value>\n  </data>\n  <data name=\"SetupConnSuccess\" xml:space=\"preserve\">\n    <value>З'єднання з базою даних було успішне. Будь ласка, заповніть наступні поля, щоб почати роботу</value>\n  </data>\n  <data name=\"SetupDatabase\" xml:space=\"preserve\">\n    <value>Налаштування бази даних</value>\n  </data>\n  <data name=\"SetupFirstUser\" xml:space=\"preserve\">\n    <value>Налаштуйте вашого першого користувача (який буде наділаний привілегіями адміна і модератора)</value>\n  </data>\n  <data name=\"SubscribedTopics\" xml:space=\"preserve\">\n    <value>Підписані теми</value>\n  </data>\n  <data name=\"SubscribeLoggedIn\" xml:space=\"preserve\">\n    <value>Ви повинні увійти в систему для перегляду підписаних тем.</value>\n  </data>\n  <data name=\"SubscribeNone\" xml:space=\"preserve\">\n    <value>Ви не підписані на жодну тему.</value>\n  </data>\n  <data name=\"Success\" xml:space=\"preserve\">\n    <value>Успіх!</value>\n  </data>\n  <data name=\"UnsubscribeTopic\" xml:space=\"preserve\">\n    <value>{0} відписався від {1}</value>\n    <comment>{0} is a name, {1} is a topic title</comment>\n  </data>\n  <data name=\"ArchivedNewPosts\" xml:space=\"preserve\">\n    <value>В архіві з новими повідомленнями</value>\n  </data>\n  <data name=\"Closed\" xml:space=\"preserve\">\n    <value>Закрита тема</value>\n  </data>\n  <data name=\"ClosedPinned\" xml:space=\"preserve\">\n    <value>Закрита, прикріплена тема</value>\n  </data>\n  <data name=\"DeleteTopic\" xml:space=\"preserve\">\n    <value>Видалити Тему</value>\n  </data>\n  <data name=\"First\" xml:space=\"preserve\">\n    <value>Перший</value>\n  </data>\n  <data name=\"ForgotPasswordEmail\" xml:space=\"preserve\">\n    <value>Щоб скинути ваш пароль на {0}, будь ласка перейдіть за посиланням:{3}{1}{3}{3}{2}</value>\n    <comment>{0}=title of forum, {1}=email link, {2}=signature, {3}=new line</comment>\n  </data>\n  <data name=\"ForgotPasswordSubject\" xml:space=\"preserve\">\n    <value>Інструкції скидання вашого паролю для {0}</value>\n    <comment>{0} is name of forum</comment>\n  </data>\n  <data name=\"ForumNoPost\" xml:space=\"preserve\">\n    <value>Ви не можете писати в цьому форумі</value>\n  </data>\n  <data name=\"ForumNoView\" xml:space=\"preserve\">\n    <value>Ви не можете переглядати цей форум</value>\n  </data>\n  <data name=\"LessThanMinute\" xml:space=\"preserve\">\n    <value>Менше хвилини назад</value>\n  </data>\n  <data name=\"LoginToPost\" xml:space=\"preserve\">\n    <value>Вам потрібно залогінитись аби відповісти</value>\n  </data>\n  <data name=\"MinutesAgo\" xml:space=\"preserve\">\n    <value>{0} хвилин назад</value>\n    <comment>{0} is a number</comment>\n  </data>\n  <data name=\"More\" xml:space=\"preserve\">\n    <value>Більше</value>\n  </data>\n  <data name=\"NewPosts\" xml:space=\"preserve\">\n    <value>Нові повідомлення</value>\n  </data>\n  <data name=\"NewPostsClosed\" xml:space=\"preserve\">\n    <value>Нові повідомлення, закрита тема</value>\n  </data>\n  <data name=\"NewPostsClosedPinned\" xml:space=\"preserve\">\n    <value>Нові повідомлення, закрита і прикріплена тема</value>\n  </data>\n  <data name=\"NewPostsPinned\" xml:space=\"preserve\">\n    <value>Нові повідомлення, прикріплена тема</value>\n  </data>\n  <data name=\"Next\" xml:space=\"preserve\">\n    <value>Наступний</value>\n  </data>\n  <data name=\"NoNewPosts\" xml:space=\"preserve\">\n    <value>Ніяких нових повідомлень</value>\n  </data>\n  <data name=\"NotLoggedIn\" xml:space=\"preserve\">\n    <value>Ви не залоговані</value>\n  </data>\n  <data name=\"OneMinuteAgo\" xml:space=\"preserve\">\n    <value>1 хвилину назад</value>\n  </data>\n  <data name=\"Pinned\" xml:space=\"preserve\">\n    <value>Закріпити тему</value>\n  </data>\n  <data name=\"PMCreateWarnings\" xml:space=\"preserve\">\n\t  <value>Повинна включати повідомлення та хоча б одного користувача</value>\n  </data>\n  <data name=\"PostEmpty\" xml:space=\"preserve\">\n    <value>Ви не можете відправити пусте повідомлення</value>\n  </data>\n  <data name=\"PostWait\" xml:space=\"preserve\">\n    <value>Вам потрібно почекати {0} секунд між створенням повідомлень і не можете повторювати ваше попереднє повідомлення</value>\n    <comment>{0} is a number for seconds</comment>\n  </data>\n  <data name=\"Previous\" xml:space=\"preserve\">\n    <value>Попередній</value>\n  </data>\n  <data name=\"RegisterEmailSubject\" xml:space=\"preserve\">\n    <value>Реєстрація на форумі - {0}</value>\n    <comment>{0} is name of forum</comment>\n  </data>\n  <data name=\"RegisterEmailThankYou\" xml:space=\"preserve\">\n    <value>Дякуємо за реєстрацію на {0}.{2}{2}{1}</value>\n    <comment>{0}=name of forum, {1}=forum link, {2}=new line</comment>\n  </data>\n  <data name=\"RegisterEmailThankYouVerify\" xml:space=\"preserve\">\n    <value>Дякуємо за реєстрацію на {0}. Будь ласка підтвердіть вашу реєстрацію за наступним посиланням:{5}{5}{1} {5}{5}Якщо ви не можете перейти за вказаним посиланням, будь ласка відвідайте наступну сторінку і скопіюйте підтверджуючий код:{5}{5}Посилання: {2} {5}Код: {3} {5}{5}{4}</value>\n    <comment>{0}=name of forum, {1}=auth link, {2}=verify link, {3}=auth code, {4}=sig, {5}new line</comment>\n  </data>\n  <data name=\"SettingsSaved\" xml:space=\"preserve\">\n    <value>Налаштування збережено</value>\n  </data>\n  <data name=\"TodayTime\" xml:space=\"preserve\">\n    <value>Сьогодні, {0}</value>\n    <comment>{0} is a time like 1:43pm</comment>\n  </data>\n  <data name=\"TopicNotExist\" xml:space=\"preserve\">\n    <value>Така тема не існує</value>\n  </data>\n  <data name=\"YesterdayTime\" xml:space=\"preserve\">\n    <value>Вчора, {0}</value>\n    <comment>{0} a time like 1:43pm</comment>\n  </data>\n  <data name=\"Loading\" xml:space=\"preserve\">\n    <value>Завантаження</value>\n  </data>\n  <data name=\"Voted\" xml:space=\"preserve\">\n    <value>Проголосований</value>\n  </data>\n  <data name=\"VoteUpPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt;проголосував за повідомлення в темі: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"ScoringGame\" xml:space=\"preserve\">\n    <value>Scoring game</value>\n  </data>\n  <data name=\"Awards\" xml:space=\"preserve\">\n    <value>Нагорода</value>\n  </data>\n  <data name=\"EventDefinitions\" xml:space=\"preserve\">\n    <value>Опис Події</value>\n  </data>\n  <data name=\"AwardDefinitions\" xml:space=\"preserve\">\n    <value>Опис Нагороди</value>\n  </data>\n  <data name=\"ManualEvent\" xml:space=\"preserve\">\n    <value>Керівництво подіями</value>\n  </data>\n  <data name=\"None\" xml:space=\"preserve\">\n    <value>Жоден</value>\n  </data>\n  <data name=\"ActivityFeed\" xml:space=\"preserve\">\n    <value>Стрічка Актовиності</value>\n  </data>\n  <data name=\"NewReplyPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; зробив повідомлення в темі: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"NewPostPublishMessage\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; розпочав нову тему: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to topic{3} is topic name</comment>\n  </data>\n  <data name=\"ExternalLogins\" xml:space=\"preserve\">\n    <value>зовнішні логінів</value>\n  </data>\n  <data name=\"UseExistingForumAccount\" xml:space=\"preserve\">\n    <value>Використання існуючої облікової форум</value>\n  </data>\n  <data name=\"ExpiredLogin\" xml:space=\"preserve\">\n    <value>Цей логін завершився, або не застосовується</value>\n  </data>\n  <data name=\"ExternalLoginsDisabled\" xml:space=\"preserve\">\n    <value>Зовнішні входи відключені</value>\n  </data>\n  <data name=\"NoExternalLoginsRegistered\" xml:space=\"preserve\">\n    <value>У вас немає зовнішніх логінів зареєстрованих</value>\n  </data>\n  <data name=\"DeletePermanently\" xml:space=\"preserve\">\n    <value>Видалити файл</value>\n  </data>\n  <data name=\"Preview\" xml:space=\"preserve\">\n    <value>Попередній</value>\n  </data>\n  <data name=\"AskAQuestion\" xml:space=\"preserve\">\n    <value>Задайте питання</value>\n  </data>\n  <data name=\"Answers\" xml:space=\"preserve\">\n    <value>відповіді</value>\n  </data>\n  <data name=\"ChooseAnswer\" xml:space=\"preserve\">\n    <value>Виберіть відповідь</value>\n  </data>\n  <data name=\"PostAnswer\" xml:space=\"preserve\">\n    <value>повідомлення Відповідь</value>\n  </data>\n  <data name=\"QuestionAnswered\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; обрали відповідь на питання: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"SubmitAnswer\" xml:space=\"preserve\">\n    <value>Додати відповідь</value>\n  </data>\n  <data name=\"AppRestartRequired\" xml:space=\"preserve\">\n    <value>потрібне перезавантаження App</value>\n  </data>\n  <data name=\"SearchError\" xml:space=\"preserve\">\n    <value>Сталася помилка пошуку</value>\n  </data>\n  <data name=\"CloseOldTopics\" xml:space=\"preserve\">\n    <value>Закрийте старі теми</value>\n  </data>\n  <data name=\"Days\" xml:space=\"preserve\">\n    <value>днів</value>\n  </data>\n  <data name=\"PrivateForum\" xml:space=\"preserve\">\n    <value>Приватний форум</value>\n  </data>\n  <data name=\"AccountNotVerified\" xml:space=\"preserve\">\n    <value>Обліковий запис не підтверджено</value>\n  </data>\n  <data name=\"SelectText\" xml:space=\"preserve\">\n\t  <value>Виберіть Текст</value>\n  </data>\n  <data name=\"Notifications\" xml:space=\"preserve\">\n\t  <value>Сповіщення</value>\n  </data>\n  <data name=\"NewReplyNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; зробив повідомлення в темі: &lt;b&gt;{1}&lt;/b&gt;\n</value>\n  </data>\n  <data name=\"Award\" xml:space=\"preserve\">\n\t  <value>Нагорода</value>\n  </data>\n  <data name=\"VoteUpNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt;проголосував за повідомлення в темі: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"QuestionAnsweredNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; обрали відповідь на питання: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"AutoFollow\" xml:space=\"preserve\">\n\t  <value>Автоматично слідкуйте за темами, на які ви відповідаєте</value>\n  </data>\n  <data name=\"MarkAllRead\" xml:space=\"preserve\">\n\t  <value>Позначити все як прочитане</value>\n  </data>\n  <data name=\"UploadImage\" xml:space=\"preserve\">\n\t  <value>Завантажити зображення</value>\n  </data>\n  <data name=\"Ignored\" xml:space=\"preserve\">\n\t  <value>Ігнорується</value>\n  </data>\n  <data name=\"IgnoreList\" xml:space=\"preserve\">\n\t  <value>Список ігнорування</value>\n  </data>\n  <data name=\"Ignore\" xml:space=\"preserve\">\n\t  <value>Ігнорувати</value>\n  </data>\n</root>"
  },
  {
    "path": "src/PopForums/Resources/Resources.zh-TW.resx",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!-- \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The primary goals of this format is to allow a simple XML format \n    that is mostly human readable. The generation and parsing of the \n    various data types are done through the TypeConverter classes \n    associated with the data types.\n    \n    Example:\n    \n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n                \n    There are any number of \"resheader\" rows that contain simple \n    name/value pairs.\n    \n    Each data row contains a name, and value. The row also contains a \n    type or mimetype. Type corresponds to a .NET class that support \n    text/value conversion through the TypeConverter architecture. \n    Classes that don't support this are serialized and stored with the \n    mimetype set.\n    \n    The mimetype is used for serialized objects, and tells the \n    ResXResourceReader how to depersist the object. This is currently not \n    extensible. For a given mimetype the value must be set accordingly:\n    \n    Note - application/x-microsoft.net.object.binary.base64 is the format \n    that the ResXResourceWriter will generate, however the reader can \n    read any of the formats listed below.\n    \n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n    \n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with \n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array \n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"Account\" xml:space=\"preserve\">\n    <value>帳戶</value>\n  </data>\n  <data name=\"AccountCreated\" xml:space=\"preserve\">\n    <value>建立帳戶</value>\n  </data>\n  <data name=\"AccountReady\" xml:space=\"preserve\">\n    <value>您的帳號已建立，您可以開始使用了。</value>\n  </data>\n  <data name=\"AccountReadyCheckEmail\" xml:space=\"preserve\">\n    <value>請檢查您的電子郵件中的確認連結。</value>\n  </data>\n  <data name=\"AccountVerified\" xml:space=\"preserve\">\n    <value>您的帳戶已確認完畢</value>\n  </data>\n  <data name=\"AlreadyCreatedAccount\" xml:space=\"preserve\">\n    <value>您已經登入，並且擁有一個帳戶。</value>\n  </data>\n  <data name=\"AsYouWouldLikeItToAppear\" xml:space=\"preserve\">\n    <value>（你想它顯示為什麼）</value>\n  </data>\n  <data name=\"AtLeastSixChar\" xml:space=\"preserve\">\n    <value>（至少6個字符）</value>\n  </data>\n  <data name=\"By\" xml:space=\"preserve\">\n    <value>作者</value>\n  </data>\n  <data name=\"CreateAccountButton\" xml:space=\"preserve\">\n    <value>建立帳戶</value>\n  </data>\n  <data name=\"CreateAnAccount\" xml:space=\"preserve\">\n    <value>建立新帳戶</value>\n  </data>\n  <data name=\"EditYourProfile\" xml:space=\"preserve\">\n    <value>編輯您的個人資料</value>\n  </data>\n  <data name=\"Email\" xml:space=\"preserve\">\n    <value>電子郵件</value>\n  </data>\n  <data name=\"EmailProblemAccount\" xml:space=\"preserve\">\n    <value>發送您的驗證郵件時出現問題：</value>\n  </data>\n  <data name=\"Favorites\" xml:space=\"preserve\">\n    <value>我的最愛</value>\n  </data>\n  <data name=\"Forbidden\" xml:space=\"preserve\">\n    <value>很抱歉，您沒有權限。</value>\n  </data>\n  <data name=\"Forums\" xml:space=\"preserve\">\n    <value>論壇</value>\n  </data>\n  <data name=\"HaveReadTOS\" xml:space=\"preserve\">\n    <value>我已閱讀並接受服務條款</value>\n  </data>\n  <data name=\"Last\" xml:space=\"preserve\">\n    <value>最後</value>\n  </data>\n  <data name=\"Login\" xml:space=\"preserve\">\n    <value>註冊</value>\n  </data>\n  <data name=\"Logout\" xml:space=\"preserve\">\n    <value>登出</value>\n  </data>\n  <data name=\"MarkAllForumsRead\" xml:space=\"preserve\">\n    <value>標記所有論壇為已讀</value>\n  </data>\n  <data name=\"MyBirthdayIsOnOrBefore\" xml:space=\"preserve\">\n    <value>我的生日是在或之前{0}</value>\n  </data>\n  <data name=\"Name\" xml:space=\"preserve\">\n    <value>名稱</value>\n  </data>\n  <data name=\"NeedToVerifyExistingAccount\" xml:space=\"preserve\">\n    <value>需要驗證現有帳戶？</value>\n  </data>\n  <data name=\"PageNotFound\" xml:space=\"preserve\">\n    <value>找不到網頁</value>\n  </data>\n  <data name=\"Password\" xml:space=\"preserve\">\n    <value>密碼</value>\n  </data>\n  <data name=\"Posts\" xml:space=\"preserve\">\n    <value>主題</value>\n  </data>\n  <data name=\"PrivateMessages\" xml:space=\"preserve\">\n    <value>私人訊息</value>\n  </data>\n  <data name=\"Recent\" xml:space=\"preserve\">\n    <value>最近</value>\n  </data>\n  <data name=\"RegisteredUsers\" xml:space=\"preserve\">\n    <value>註冊用戶</value>\n  </data>\n  <data name=\"RetypePassword\" xml:space=\"preserve\">\n    <value>重新輸入密碼</value>\n  </data>\n  <data name=\"Search\" xml:space=\"preserve\">\n    <value>搜尋</value>\n  </data>\n  <data name=\"SubscribeToList\" xml:space=\"preserve\">\n    <value>訂閱郵件列表</value>\n  </data>\n  <data name=\"Subscriptions\" xml:space=\"preserve\">\n    <value>訂閱</value>\n  </data>\n  <data name=\"TermsOfService\" xml:space=\"preserve\">\n    <value>服務條款</value>\n  </data>\n  <data name=\"TimeZone\" xml:space=\"preserve\">\n    <value>時區</value>\n  </data>\n  <data name=\"Topics\" xml:space=\"preserve\">\n    <value>主題</value>\n  </data>\n  <data name=\"Total\" xml:space=\"preserve\">\n    <value>總數</value>\n  </data>\n  <data name=\"TotalPosts\" xml:space=\"preserve\">\n    <value>文章總數</value>\n  </data>\n  <data name=\"TotalTopics\" xml:space=\"preserve\">\n    <value>主題總數</value>\n  </data>\n  <data name=\"UseDaylight\" xml:space=\"preserve\">\n    <value>使用夏令時</value>\n  </data>\n  <data name=\"UsersOnline\" xml:space=\"preserve\">\n    <value>上線人數</value>\n  </data>\n  <data name=\"MustBe13\" xml:space=\"preserve\">\n    <value>您必須年滿13歲才能註冊</value>\n  </data>\n  <data name=\"MustAcceptTOS\" xml:space=\"preserve\">\n    <value>您必須接受服務條款</value>\n  </data>\n  <data name=\"RetypeYourPassword\" xml:space=\"preserve\">\n    <value>請重新輸入您的密碼</value>\n  </data>\n  <data name=\"NameRequired\" xml:space=\"preserve\">\n    <value>名稱為必填</value>\n  </data>\n  <data name=\"NameInUse\" xml:space=\"preserve\">\n    <value>該名稱已被使用</value>\n  </data>\n  <data name=\"EmailRequired\" xml:space=\"preserve\">\n    <value>E-mail為必填</value>\n  </data>\n  <data name=\"ValidEmailAddressRequired\" xml:space=\"preserve\">\n    <value>E-mail無效</value>\n  </data>\n  <data name=\"EmailInUse\" xml:space=\"preserve\">\n    <value>該E-mail地址已被使用</value>\n  </data>\n  <data name=\"EmailBanned\" xml:space=\"preserve\">\n    <value>該E-mail地址已被封鎖</value>\n  </data>\n  <data name=\"IPBanned\" xml:space=\"preserve\">\n    <value>您的IP地址已被封鎖</value>\n  </data>\n  <data name=\"RetypePasswordMustMatch\" xml:space=\"preserve\">\n    <value>您輸入的密碼不一致</value>\n  </data>\n  <data name=\"EditAccount\" xml:space=\"preserve\">\n    <value>編輯帳戶</value>\n  </data>\n  <data name=\"EditProfile\" xml:space=\"preserve\">\n    <value>編輯個人資料</value>\n  </data>\n  <data name=\"EnterVerificationCode\" xml:space=\"preserve\">\n    <value>請輸入您的驗證碼</value>\n  </data>\n  <data name=\"ForumHomePage\" xml:space=\"preserve\">\n    <value>論壇首頁</value>\n  </data>\n  <data name=\"MustBeRegisteredToEditAccount\" xml:space=\"preserve\">\n    <value>您必須註冊並登入編輯您的帳戶。</value>\n  </data>\n  <data name=\"NoUserFoundWithEmail\" xml:space=\"preserve\">\n    <value>找不到用戶與郵件</value>\n  </data>\n  <data name=\"SendEmailWithNewCodeButton\" xml:space=\"preserve\">\n    <value>發送E-mail隨著新守則</value>\n  </data>\n  <data name=\"VerificationEmailSent\" xml:space=\"preserve\">\n    <value>您的驗證郵件已發送</value>\n  </data>\n  <data name=\"VerificationFailure\" xml:space=\"preserve\">\n    <value>驗證失敗</value>\n  </data>\n  <data name=\"VerificationIfYouNeed\" xml:space=\"preserve\">\n    <value>如果你需要一個新的驗證碼，請在此輸入您的電子郵件</value>\n  </data>\n  <data name=\"VerificationLinkBad\" xml:space=\"preserve\">\n    <value>對不起，您輸入的驗證鏈接或代碼無效。。</value>\n  </data>\n  <data name=\"VerifyAccount\" xml:space=\"preserve\">\n    <value>驗證帳戶</value>\n  </data>\n  <data name=\"VerifyCodeButton\" xml:space=\"preserve\">\n    <value>驗證碼</value>\n  </data>\n  <data name=\"Avatar\" xml:space=\"preserve\">\n    <value>頭像</value>\n  </data>\n  <data name=\"AvatarDelete\" xml:space=\"preserve\">\n    <value>刪除此頭像</value>\n  </data>\n  <data name=\"ChangeEmail\" xml:space=\"preserve\">\n    <value>更改電子郵件</value>\n  </data>\n  <data name=\"ChangeEmailButton\" xml:space=\"preserve\">\n    <value>更改電子郵件和發送代碼</value>\n  </data>\n  <data name=\"ChangeEmailConsequence\" xml:space=\"preserve\">\n    <value>改變您的電子郵件地址將導致生成並發送至新的電子郵件地址一個新的驗證碼。你將無法發表，直到您用新的代碼驗證您的電子郵件地址。</value>\n  </data>\n  <data name=\"ChangePassword\" xml:space=\"preserve\">\n    <value>更改密碼</value>\n  </data>\n  <data name=\"ChangeYourEmailPassword\" xml:space=\"preserve\">\n    <value>更改您的電子郵件或密碼</value>\n  </data>\n  <data name=\"DateOfBirth\" xml:space=\"preserve\">\n    <value>出生日期</value>\n  </data>\n  <data name=\"Details\" xml:space=\"preserve\">\n    <value>詳細資訊</value>\n  </data>\n  <data name=\"EmailChangeSuccess\" xml:space=\"preserve\">\n    <value>您的電子郵件已成功更改</value>\n  </data>\n  <data name=\"EmailNew\" xml:space=\"preserve\">\n    <value>新的E-mail地址</value>\n  </data>\n  <data name=\"EmailNewRetype\" xml:space=\"preserve\">\n    <value>重新輸入新的電子郵件地址</value>\n  </data>\n  <data name=\"EmailNotFound\" xml:space=\"preserve\">\n    <value>電子郵件未找到</value>\n  </data>\n  <data name=\"EmailSent\" xml:space=\"preserve\">\n    <value>您的電子郵件已發送</value>\n  </data>\n  <data name=\"EmailsMustMatch\" xml:space=\"preserve\">\n    <value>這些電子郵件地址不匹配</value>\n  </data>\n  <data name=\"EmailUser\" xml:space=\"preserve\">\n    <value>E-mail用戶</value>\n  </data>\n  <data name=\"ForcePlainTextBox\" xml:space=\"preserve\">\n    <value>強制純文字框</value>\n  </data>\n  <data name=\"ForgotInstructions\" xml:space=\"preserve\">\n    <value>輸入與您的帳戶關聯的電子郵件地址，接收重置您的密碼的指令：</value>\n  </data>\n  <data name=\"ForgotInstructionsSent\" xml:space=\"preserve\">\n    <value>說明已發送到您的電子郵件</value>\n  </data>\n  <data name=\"ForgotPassword\" xml:space=\"preserve\">\n    <value>忘記密碼</value>\n  </data>\n  <data name=\"ForgotPasswordQuestion\" xml:space=\"preserve\">\n    <value>忘記密碼？</value>\n  </data>\n  <data name=\"HideVanity\" xml:space=\"preserve\">\n    <value>隐藏头像和签名</value>\n  </data>\n  <data name=\"In\" xml:space=\"preserve\">\n    <value>在</value>\n  </data>\n  <data name=\"Joined\" xml:space=\"preserve\">\n    <value>已加入</value>\n  </data>\n  <data name=\"Location\" xml:space=\"preserve\">\n    <value>位置</value>\n  </data>\n  <data name=\"LoginAlready\" xml:space=\"preserve\">\n    <value>您已經登入。</value>\n  </data>\n  <data name=\"LoginBad\" xml:space=\"preserve\">\n    <value>錯誤的電子郵件或密碼</value>\n  </data>\n  <data name=\"ManagePhotos\" xml:space=\"preserve\">\n    <value>管理照片</value>\n  </data>\n  <data name=\"ManageYourPhotos\" xml:space=\"preserve\">\n    <value>管理您的照片</value>\n  </data>\n  <data name=\"NewPasswordSaved\" xml:space=\"preserve\">\n    <value>新密碼已儲存</value>\n  </data>\n  <data name=\"NotRegisteredQuestion\" xml:space=\"preserve\">\n    <value>沒有註冊？</value>\n  </data>\n  <data name=\"OldPasswordIncorrect\" xml:space=\"preserve\">\n    <value>舊密碼不正確</value>\n  </data>\n  <data name=\"Options\" xml:space=\"preserve\">\n    <value>選項</value>\n  </data>\n  <data name=\"PasswordNew\" xml:space=\"preserve\">\n    <value>新密碼</value>\n  </data>\n  <data name=\"PasswordNewRetype\" xml:space=\"preserve\">\n    <value>重新輸入新密碼</value>\n  </data>\n  <data name=\"PasswordOld\" xml:space=\"preserve\">\n    <value>舊密碼</value>\n  </data>\n  <data name=\"PasswordReset\" xml:space=\"preserve\">\n    <value>重設密碼</value>\n  </data>\n  <data name=\"PasswordResetLinkInvalid\" xml:space=\"preserve\">\n    <value>這不是一個有效的密碼重置連結。</value>\n  </data>\n  <data name=\"PasswordResetNote\" xml:space=\"preserve\">\n    <value>你的新密碼生效，現在你已經登錄。</value>\n  </data>\n  <data name=\"PasswordResetSuccess\" xml:space=\"preserve\">\n    <value>密碼重置成功</value>\n  </data>\n  <data name=\"Photo\" xml:space=\"preserve\">\n    <value>照片</value>\n  </data>\n  <data name=\"PhotoDelete\" xml:space=\"preserve\">\n    <value>刪除照片</value>\n  </data>\n  <data name=\"PhotoNotApproved\" xml:space=\"preserve\">\n    <value>（這個影像還未被管理員核准）</value>\n  </data>\n  <data name=\"ProfileUpdated\" xml:space=\"preserve\">\n    <value>資料更新</value>\n  </data>\n  <data name=\"RememberMe\" xml:space=\"preserve\">\n    <value>還記得我嗎？</value>\n  </data>\n  <data name=\"Replies\" xml:space=\"preserve\">\n    <value>回覆</value>\n  </data>\n  <data name=\"Save\" xml:space=\"preserve\">\n    <value>儲存</value>\n  </data>\n  <data name=\"SendEmailButton\" xml:space=\"preserve\">\n    <value>發送E-mail</value>\n  </data>\n  <data name=\"SendPM\" xml:space=\"preserve\">\n    <value>發送私人訊息</value>\n  </data>\n  <data name=\"ShowProfileDetails\" xml:space=\"preserve\">\n    <value>顯示您的個人資料（但不是電子郵件）</value>\n  </data>\n  <data name=\"Signature\" xml:space=\"preserve\">\n    <value>簽名</value>\n  </data>\n  <data name=\"StartedBy\" xml:space=\"preserve\">\n    <value>開始</value>\n  </data>\n  <data name=\"Subject\" xml:space=\"preserve\">\n    <value>主題</value>\n  </data>\n  <data name=\"Text\" xml:space=\"preserve\">\n    <value>文字</value>\n  </data>\n  <data name=\"To\" xml:space=\"preserve\">\n    <value>至</value>\n  </data>\n  <data name=\"UploadNew\" xml:space=\"preserve\">\n    <value>上傳新項目</value>\n  </data>\n  <data name=\"UserNotFound\" xml:space=\"preserve\">\n    <value>找不到用戶</value>\n  </data>\n  <data name=\"Views\" xml:space=\"preserve\">\n    <value>瀏覽次數</value>\n  </data>\n  <data name=\"Web\" xml:space=\"preserve\">\n    <value>Web</value>\n  </data>\n  <data name=\"WebVisit\" xml:space=\"preserve\">\n    <value>用戶訪問的Web站點</value>\n  </data>\n  <data name=\"YourIP\" xml:space=\"preserve\">\n    <value>您的IP地址</value>\n  </data>\n  <data name=\"Birthday\" xml:space=\"preserve\">\n    <value>生日</value>\n  </data>\n  <data name=\"Contact\" xml:space=\"preserve\">\n    <value>聯絡</value>\n  </data>\n  <data name=\"Profile\" xml:space=\"preserve\">\n    <value>個人資料</value>\n  </data>\n  <data name=\"SendNameEmail\" xml:space=\"preserve\">\n    <value>發送{0}的電子郵件</value>\n  </data>\n  <data name=\"SendNamePM\" xml:space=\"preserve\">\n    <value>發送{0}私人訊息</value>\n  </data>\n  <data name=\"Unsubscribe\" xml:space=\"preserve\">\n    <value>退訂</value>\n  </data>\n  <data name=\"UnsubscribeFail\" xml:space=\"preserve\">\n    <value>退訂失敗</value>\n  </data>\n  <data name=\"UnsubscribeLinkBad\" xml:space=\"preserve\">\n    <value>這個退訂連結是無效的。</value>\n  </data>\n  <data name=\"UnsubscribeNote\" xml:space=\"preserve\">\n    <value>您的退訂請求已經被處理。您可以透過登入並進入你的帳戶頁面重新訂閱。</value>\n  </data>\n  <data name=\"AddForum\" xml:space=\"preserve\">\n    <value>加入論壇</value>\n  </data>\n  <data name=\"AddNew\" xml:space=\"preserve\">\n    <value>新增項目</value>\n  </data>\n  <data name=\"AllowImages\" xml:space=\"preserve\">\n    <value>允許圖片</value>\n  </data>\n  <data name=\"Archived\" xml:space=\"preserve\">\n    <value>已封存</value>\n  </data>\n  <data name=\"AreYouSure\" xml:space=\"preserve\">\n    <value>你確定嗎？</value>\n  </data>\n  <data name=\"Ban\" xml:space=\"preserve\">\n    <value>封鎖</value>\n  </data>\n  <data name=\"BanRemove\" xml:space=\"preserve\">\n    <value>移除封鎖</value>\n  </data>\n  <data name=\"Categories\" xml:space=\"preserve\">\n    <value>分類</value>\n  </data>\n  <data name=\"Category\" xml:space=\"preserve\">\n    <value>類別</value>\n  </data>\n  <data name=\"CategoryEditTitle\" xml:space=\"preserve\">\n    <value>編輯類別名稱</value>\n  </data>\n  <data name=\"CategoryMoveError\" xml:space=\"preserve\">\n    <value>嘗試移動類別發生錯誤</value>\n  </data>\n  <data name=\"CensorReplacementChar\" xml:space=\"preserve\">\n    <value>審查替換字元</value>\n  </data>\n  <data name=\"CensorWords\" xml:space=\"preserve\">\n    <value>審查文字</value>\n  </data>\n  <data name=\"Delete\" xml:space=\"preserve\">\n    <value>刪除</value>\n  </data>\n  <data name=\"DeleteAndBanButton\" xml:space=\"preserve\">\n    <value>刪除和封鎖用戶</value>\n  </data>\n  <data name=\"DeleteUserButton\" xml:space=\"preserve\">\n    <value>刪除用戶</value>\n  </data>\n  <data name=\"Description\" xml:space=\"preserve\">\n    <value>描述</value>\n  </data>\n  <data name=\"Down\" xml:space=\"preserve\">\n    <value>向下</value>\n  </data>\n  <data name=\"Edit\" xml:space=\"preserve\">\n    <value>編輯</value>\n  </data>\n  <data name=\"EditForum\" xml:space=\"preserve\">\n    <value>編輯論壇</value>\n  </data>\n  <data name=\"EditUser\" xml:space=\"preserve\">\n    <value>編輯用戶</value>\n  </data>\n  <data name=\"EmailBan\" xml:space=\"preserve\">\n    <value>電子郵件封鎖</value>\n  </data>\n  <data name=\"EmailIpBan\" xml:space=\"preserve\">\n    <value>E-mail/IP封鎖</value>\n  </data>\n  <data name=\"EmailNewOptional\" xml:space=\"preserve\">\n    <value>新的電子郵件（可選）</value>\n  </data>\n  <data name=\"EmailUsers\" xml:space=\"preserve\">\n    <value>電子郵箱用戶</value>\n  </data>\n  <data name=\"ErrorLog\" xml:space=\"preserve\">\n    <value>錯誤紀錄</value>\n  </data>\n  <data name=\"Forum\" xml:space=\"preserve\">\n    <value>論壇</value>\n  </data>\n  <data name=\"ForumAdapter\" xml:space=\"preserve\">\n    <value>論壇適配器（可選，使用「 Namespace.Type ，的AssemblyName 」 ）</value>\n  </data>\n  <data name=\"ForumHome\" xml:space=\"preserve\">\n    <value>論壇首頁</value>\n  </data>\n  <data name=\"ForumPermisions\" xml:space=\"preserve\">\n    <value>論壇權限</value>\n  </data>\n  <data name=\"ForumSettings\" xml:space=\"preserve\">\n    <value>論壇設定</value>\n  </data>\n  <data name=\"ForumsUncat\" xml:space=\"preserve\">\n    <value>未分類論壇</value>\n  </data>\n  <data name=\"GeneralSettings\" xml:space=\"preserve\">\n    <value>一般設定</value>\n  </data>\n  <data name=\"IpBan\" xml:space=\"preserve\">\n    <value>封鎖的IP</value>\n  </data>\n  <data name=\"IpHistory\" xml:space=\"preserve\">\n    <value>IP歷史記錄</value>\n  </data>\n  <data name=\"IsApproved\" xml:space=\"preserve\">\n    <value>已核准</value>\n  </data>\n  <data name=\"IsSubscribed\" xml:space=\"preserve\">\n    <value>已訂閱</value>\n  </data>\n  <data name=\"LogErrors\" xml:space=\"preserve\">\n    <value>記錄錯誤</value>\n  </data>\n  <data name=\"Logging\" xml:space=\"preserve\">\n    <value>日誌</value>\n  </data>\n  <data name=\"LogMod\" xml:space=\"preserve\">\n    <value>管理日誌</value>\n  </data>\n  <data name=\"LogSecurity\" xml:space=\"preserve\">\n    <value>登錄安全活動</value>\n  </data>\n  <data name=\"MinimumTimeBetweenPosts\" xml:space=\"preserve\">\n    <value>發布文章之間的最小時間（秒）</value>\n  </data>\n  <data name=\"ModerationLog\" xml:space=\"preserve\">\n    <value>管理日誌</value>\n  </data>\n  <data name=\"Move\" xml:space=\"preserve\">\n    <value>移動</value>\n  </data>\n  <data name=\"NewUserApprovedWithoutVerification\" xml:space=\"preserve\">\n    <value>新用戶將被核准無需驗證</value>\n  </data>\n  <data name=\"NewUserImageApprovedWithoutMod\" xml:space=\"preserve\">\n    <value>新用戶圖像核准無須管理</value>\n  </data>\n  <data name=\"Parsing\" xml:space=\"preserve\">\n    <value>解析</value>\n  </data>\n  <data name=\"PasswordNewOptional\" xml:space=\"preserve\">\n    <value>新密碼（可選）</value>\n  </data>\n  <data name=\"PopForumsAdmin\" xml:space=\"preserve\">\n    <value>POP 論壇管理</value>\n  </data>\n  <data name=\"PostsPerPage\" xml:space=\"preserve\">\n    <value>每頁主題</value>\n  </data>\n  <data name=\"Role\" xml:space=\"preserve\">\n    <value>角色</value>\n  </data>\n  <data name=\"Roles\" xml:space=\"preserve\">\n    <value>角色</value>\n  </data>\n  <data name=\"Security\" xml:space=\"preserve\">\n    <value>安全</value>\n  </data>\n  <data name=\"SecurityLog\" xml:space=\"preserve\">\n    <value>安全日誌</value>\n  </data>\n  <data name=\"ServerTime\" xml:space=\"preserve\">\n    <value>伺服器時間</value>\n  </data>\n  <data name=\"Services\" xml:space=\"preserve\">\n    <value>服務</value>\n  </data>\n  <data name=\"SessionLength\" xml:space=\"preserve\">\n    <value>Session長度（分鐘）</value>\n  </data>\n  <data name=\"Size\" xml:space=\"preserve\">\n    <value>大小</value>\n  </data>\n  <data name=\"SubjectAndBodyNotEmpty\" xml:space=\"preserve\">\n    <value>主題和內文不能為空</value>\n  </data>\n  <data name=\"Title\" xml:space=\"preserve\">\n    <value>標題</value>\n  </data>\n  <data name=\"TopicsPerPage\" xml:space=\"preserve\">\n    <value>每頁主題</value>\n  </data>\n  <data name=\"Up\" xml:space=\"preserve\">\n    <value>向上</value>\n  </data>\n  <data name=\"UserAvatarMaxDim\" xml:space=\"preserve\">\n    <value>頭像最大尺寸</value>\n  </data>\n  <data name=\"UserDeleteWarning\" xml:space=\"preserve\">\n    <value>警告！刪除帳戶是無法復原的</value>\n  </data>\n  <data name=\"UserImageApproval\" xml:space=\"preserve\">\n    <value>用戶圖像核可</value>\n  </data>\n  <data name=\"UserImageMaxDim\" xml:space=\"preserve\">\n    <value>用戶圖片最大尺寸</value>\n  </data>\n  <data name=\"UserRoles\" xml:space=\"preserve\">\n    <value>用戶角色</value>\n  </data>\n  <data name=\"Visible\" xml:space=\"preserve\">\n    <value>可見</value>\n  </data>\n  <data name=\"AddNewForum\" xml:space=\"preserve\">\n    <value>新增一個新的論壇</value>\n  </data>\n  <data name=\"Body\" xml:space=\"preserve\">\n    <value>內文</value>\n  </data>\n  <data name=\"BodyHtml\" xml:space=\"preserve\">\n    <value>HTML內文</value>\n  </data>\n  <data name=\"ClickToLoadMorePosts\" xml:space=\"preserve\">\n    <value>點擊載入更多主題</value>\n  </data>\n  <data name=\"Comment\" xml:space=\"preserve\">\n    <value>評論</value>\n  </data>\n  <data name=\"CommentsOptional\" xml:space=\"preserve\">\n    <value>評論（可選）</value>\n  </data>\n  <data name=\"CreateJunkWordButton\" xml:space=\"preserve\">\n    <value>建立垃圾字</value>\n  </data>\n  <data name=\"CreateNewReply\" xml:space=\"preserve\">\n    <value>建立您的回覆</value>\n  </data>\n  <data name=\"CreateNewRole\" xml:space=\"preserve\">\n    <value>建立新角色</value>\n  </data>\n  <data name=\"CreateNewTopic\" xml:space=\"preserve\">\n    <value>建立新主題</value>\n  </data>\n  <data name=\"DeleteAllErrors\" xml:space=\"preserve\">\n    <value>刪除所有的錯誤</value>\n  </data>\n  <data name=\"DeleteJunkWordButton\" xml:space=\"preserve\">\n    <value>刪除選定垃圾字</value>\n  </data>\n  <data name=\"DeleteSelectedRole\" xml:space=\"preserve\">\n    <value>刪除選定的角色</value>\n  </data>\n  <data name=\"EditPost\" xml:space=\"preserve\">\n    <value>編輯主題</value>\n  </data>\n  <data name=\"EmailUsersQueued\" xml:space=\"preserve\">\n    <value>寄給訂閱用戶的 Email 已加入佇列</value>\n  </data>\n  <data name=\"EndDate\" xml:space=\"preserve\">\n    <value>結束日期</value>\n  </data>\n  <data name=\"Event\" xml:space=\"preserve\">\n    <value>事件</value>\n  </data>\n  <data name=\"EventTime\" xml:space=\"preserve\">\n    <value>活動時間</value>\n  </data>\n  <data name=\"FavoriteMustBeLoggedIn\" xml:space=\"preserve\">\n    <value>您必須先登入才能查看喜愛的主題。</value>\n  </data>\n  <data name=\"FavoritesDontHave\" xml:space=\"preserve\">\n    <value>你沒有任何喜歡的主題。</value>\n  </data>\n  <data name=\"FavoriteTopics\" xml:space=\"preserve\">\n    <value>最喜歡的主題</value>\n  </data>\n  <data name=\"ForumMoveError\" xml:space=\"preserve\">\n    <value>而試圖移動論壇有未知錯誤</value>\n  </data>\n  <data name=\"ForumPermissionInstructions\" xml:space=\"preserve\">\n    <value>要刪除張貼或查看限制，從適當的框中刪除所有角色。</value>\n  </data>\n  <data name=\"FromEmailAddress\" xml:space=\"preserve\">\n    <value>從E-mail地址</value>\n  </data>\n  <data name=\"ID\" xml:space=\"preserve\">\n    <value>ID</value>\n  </data>\n  <data name=\"IncludeSignature\" xml:space=\"preserve\">\n    <value>包含簽名</value>\n  </data>\n  <data name=\"IndexingInterval\" xml:space=\"preserve\">\n    <value>索引間隔</value>\n  </data>\n  <data name=\"IP\" xml:space=\"preserve\">\n    <value>知識產權</value>\n  </data>\n  <data name=\"IsRunning\" xml:space=\"preserve\">\n    <value>正在運行</value>\n  </data>\n  <data name=\"JunkWords\" xml:space=\"preserve\">\n    <value>垃圾詞</value>\n  </data>\n  <data name=\"MarkForumRead\" xml:space=\"preserve\">\n    <value>標記論壇為已讀</value>\n  </data>\n  <data name=\"Message\" xml:space=\"preserve\">\n    <value>資訊</value>\n  </data>\n  <data name=\"Millseconds\" xml:space=\"preserve\">\n    <value>毫秒</value>\n  </data>\n  <data name=\"NamePosts\" xml:space=\"preserve\">\n    <value>{0}的主題</value>\n  </data>\n  <data name=\"Optional\" xml:space=\"preserve\">\n    <value>（可選）</value>\n  </data>\n  <data name=\"PermRoles\" xml:space=\"preserve\">\n    <value>管理員和主持人是您永久的角色，而不能被刪除。</value>\n  </data>\n  <data name=\"PostID\" xml:space=\"preserve\">\n    <value>資訊ID</value>\n  </data>\n  <data name=\"PostingRoles\" xml:space=\"preserve\">\n    <value>張貼角色</value>\n  </data>\n  <data name=\"PostNewTopic\" xml:space=\"preserve\">\n    <value>發表新主題</value>\n  </data>\n  <data name=\"PreviewTopic\" xml:space=\"preserve\">\n    <value>預覽主題</value>\n  </data>\n  <data name=\"Remove\" xml:space=\"preserve\">\n    <value>清除</value>\n  </data>\n  <data name=\"RemoveAll\" xml:space=\"preserve\">\n    <value>全部刪除</value>\n  </data>\n  <data name=\"SendingInterval\" xml:space=\"preserve\">\n    <value>發送間隔</value>\n  </data>\n  <data name=\"SmtpPassword\" xml:space=\"preserve\">\n    <value>SMTP密碼</value>\n  </data>\n  <data name=\"SmtpPort\" xml:space=\"preserve\">\n    <value>端口（默認為25 ）</value>\n  </data>\n  <data name=\"SmtpServer\" xml:space=\"preserve\">\n    <value>SMTP伺服器</value>\n  </data>\n  <data name=\"SmtpUser\" xml:space=\"preserve\">\n    <value>SMTP用戶</value>\n  </data>\n  <data name=\"StartDate\" xml:space=\"preserve\">\n    <value>開始日期</value>\n  </data>\n  <data name=\"SubmitNewTopic\" xml:space=\"preserve\">\n    <value>提交新話題</value>\n  </data>\n  <data name=\"SubmitReply\" xml:space=\"preserve\">\n    <value>提交回覆</value>\n  </data>\n  <data name=\"TopicID\" xml:space=\"preserve\">\n    <value>TopicID</value>\n  </data>\n  <data name=\"Type\" xml:space=\"preserve\">\n    <value>類型</value>\n  </data>\n  <data name=\"UseEsmtpCred\" xml:space=\"preserve\">\n    <value>使用ESMTP憑證</value>\n  </data>\n  <data name=\"UserID\" xml:space=\"preserve\">\n    <value>帳號</value>\n  </data>\n  <data name=\"UserIDTarget\" xml:space=\"preserve\">\n    <value>目標帳號</value>\n  </data>\n  <data name=\"UserImageApprovalNotReq\" xml:space=\"preserve\">\n    <value>用戶上傳的圖片目前不需要核准</value>\n  </data>\n  <data name=\"UseSsl\" xml:space=\"preserve\">\n    <value>使用SSL</value>\n  </data>\n  <data name=\"ViewingRoles\" xml:space=\"preserve\">\n    <value>查看角色</value>\n  </data>\n  <data name=\"Close\" xml:space=\"preserve\">\n    <value>關閉</value>\n  </data>\n  <data name=\"CloseOnReply\" xml:space=\"preserve\">\n    <value>關閉回應</value>\n  </data>\n  <data name=\"FavoriteMake\" xml:space=\"preserve\">\n    <value>設為收藏</value>\n  </data>\n  <data name=\"FavoriteRemove\" xml:space=\"preserve\">\n    <value>從收藏夾中刪除</value>\n  </data>\n  <data name=\"Link\" xml:space=\"preserve\">\n    <value>連結</value>\n  </data>\n  <data name=\"Moderator\" xml:space=\"preserve\">\n    <value>管理者</value>\n  </data>\n  <data name=\"NameAvatar\" xml:space=\"preserve\">\n    <value>{0}的頭像</value>\n  </data>\n  <data name=\"NameLastEdit\" xml:space=\"preserve\">\n    <value>最後編輯: {0}</value>\n  </data>\n  <data name=\"Open\" xml:space=\"preserve\">\n    <value>開</value>\n  </data>\n  <data name=\"Pin\" xml:space=\"preserve\">\n    <value>釘選</value>\n  </data>\n  <data name=\"PostReply\" xml:space=\"preserve\">\n    <value>發表回覆</value>\n  </data>\n  <data name=\"Quote\" xml:space=\"preserve\">\n    <value>引用</value>\n  </data>\n  <data name=\"RecentTopics\" xml:space=\"preserve\">\n    <value>最近的話題</value>\n  </data>\n  <data name=\"Reply\" xml:space=\"preserve\">\n    <value>回覆</value>\n  </data>\n  <data name=\"ShowMorePosts\" xml:space=\"preserve\">\n    <value>顯示更多主題</value>\n  </data>\n  <data name=\"ShowPreviousPosts\" xml:space=\"preserve\">\n    <value>顯示以前的主題</value>\n  </data>\n  <data name=\"Subscribe\" xml:space=\"preserve\">\n    <value>訂閱</value>\n  </data>\n  <data name=\"Undelete\" xml:space=\"preserve\">\n    <value>取消刪除</value>\n  </data>\n  <data name=\"Unpin\" xml:space=\"preserve\">\n    <value>取消固定</value>\n  </data>\n  <data name=\"Update\" xml:space=\"preserve\">\n    <value>更新</value>\n  </data>\n  <data name=\"Archive\" xml:space=\"preserve\">\n    <value>封存</value>\n  </data>\n  <data name=\"NewPM\" xml:space=\"preserve\">\n    <value>新的私人資訊</value>\n  </data>\n  <data name=\"NoResults\" xml:space=\"preserve\">\n    <value>沒有找到結果</value>\n  </data>\n  <data name=\"Send\" xml:space=\"preserve\">\n    <value>發送</value>\n  </data>\n  <data name=\"Unarchive\" xml:space=\"preserve\">\n    <value>取消封存</value>\n  </data>\n  <data name=\"ViewArchivedMessages\" xml:space=\"preserve\">\n    <value>查看已歸檔的消息</value>\n  </data>\n  <data name=\"DisplayName\" xml:space=\"preserve\">\n    <value>顯示名稱</value>\n  </data>\n  <data name=\"Error\" xml:space=\"preserve\">\n    <value>錯誤</value>\n  </data>\n  <data name=\"ErrorSettingUpDb\" xml:space=\"preserve\">\n    <value>設定數據庫有錯誤</value>\n  </data>\n  <data name=\"ForumReady\" xml:space=\"preserve\">\n    <value>您的論壇已準備就緒。</value>\n  </data>\n  <data name=\"ForumTitle\" xml:space=\"preserve\">\n    <value>論壇標題</value>\n  </data>\n  <data name=\"GoAdmin\" xml:space=\"preserve\">\n    <value>進入管理部分來完成設定您的論壇。</value>\n  </data>\n  <data name=\"InvalidLink\" xml:space=\"preserve\">\n    <value>該連結不再有效。</value>\n  </data>\n  <data name=\"NoDataConnection\" xml:space=\"preserve\">\n    <value>沒有數據連接</value>\n  </data>\n  <data name=\"PopForumsSetup\" xml:space=\"preserve\">\n    <value>POP Forums 設定</value>\n  </data>\n  <data name=\"SetupCantConnect\" xml:space=\"preserve\">\n    <value>安裝程序無法連接到數據存儲區。請檢查您的連接字符串和配置。</value>\n  </data>\n  <data name=\"SetupConnSuccess\" xml:space=\"preserve\">\n    <value>數據庫連接是成功的。請填寫下面的值</value>\n  </data>\n  <data name=\"SetupDatabase\" xml:space=\"preserve\">\n    <value>設定數據庫</value>\n  </data>\n  <data name=\"SetupFirstUser\" xml:space=\"preserve\">\n    <value>設定您的第一個用戶（將被授予管理員和主持人權）</value>\n  </data>\n  <data name=\"SubscribedTopics\" xml:space=\"preserve\">\n    <value>訂閱主題</value>\n  </data>\n  <data name=\"SubscribeLoggedIn\" xml:space=\"preserve\">\n    <value>您必須先登入才能查看訂閱的主題。</value>\n  </data>\n  <data name=\"SubscribeNone\" xml:space=\"preserve\">\n    <value>您還沒有訂閱任何主題。</value>\n  </data>\n  <data name=\"Success\" xml:space=\"preserve\">\n    <value>成功！</value>\n  </data>\n  <data name=\"UnsubscribeTopic\" xml:space=\"preserve\">\n    <value>{0}是{1}退訂</value>\n  </data>\n  <data name=\"ArchivedNewPosts\" xml:space=\"preserve\">\n    <value>封存新主題</value>\n  </data>\n  <data name=\"Closed\" xml:space=\"preserve\">\n    <value>鎖定的主題</value>\n  </data>\n  <data name=\"ClosedPinned\" xml:space=\"preserve\">\n    <value>封閉的，固定的話題</value>\n  </data>\n  <data name=\"DeleteTopic\" xml:space=\"preserve\">\n    <value>刪除主題</value>\n  </data>\n  <data name=\"First\" xml:space=\"preserve\">\n    <value>第一</value>\n  </data>\n  <data name=\"ForgotPasswordEmail\" xml:space=\"preserve\">\n    <value>要在{0}重置您的密碼，請點擊此連結： {3} {3} {3} {2} {1}</value>\n  </data>\n  <data name=\"ForgotPasswordSubject\" xml:space=\"preserve\">\n    <value>您的密碼重置說明{0}</value>\n  </data>\n  <data name=\"ForumNoPost\" xml:space=\"preserve\">\n    <value>你不可以在這個論壇發佈</value>\n  </data>\n  <data name=\"ForumNoView\" xml:space=\"preserve\">\n    <value>您無權查看該論壇</value>\n  </data>\n  <data name=\"LessThanMinute\" xml:space=\"preserve\">\n    <value>不到一分鐘</value>\n  </data>\n  <data name=\"LoginToPost\" xml:space=\"preserve\">\n    <value>您必須登入後才能發表</value>\n  </data>\n  <data name=\"MinutesAgo\" xml:space=\"preserve\">\n    <value>{0}分鐘前</value>\n  </data>\n  <data name=\"More\" xml:space=\"preserve\">\n    <value>更多</value>\n  </data>\n  <data name=\"NewPosts\" xml:space=\"preserve\">\n    <value>新主題</value>\n  </data>\n  <data name=\"NewPostsClosed\" xml:space=\"preserve\">\n    <value>新主題已關閉</value>\n  </data>\n  <data name=\"NewPostsClosedPinned\" xml:space=\"preserve\">\n    <value>新主題已關閉已釘選</value>\n  </data>\n  <data name=\"NewPostsPinned\" xml:space=\"preserve\">\n    <value>新主題已釘選</value>\n  </data>\n  <data name=\"Next\" xml:space=\"preserve\">\n    <value>下一個</value>\n  </data>\n  <data name=\"NoNewPosts\" xml:space=\"preserve\">\n    <value>沒有新主題</value>\n  </data>\n  <data name=\"NotLoggedIn\" xml:space=\"preserve\">\n    <value>您還沒有登入</value>\n  </data>\n  <data name=\"OneMinuteAgo\" xml:space=\"preserve\">\n    <value>1分鐘前</value>\n  </data>\n  <data name=\"Pinned\" xml:space=\"preserve\">\n    <value>已釘選</value>\n  </data>\n  <data name=\"PMCreateWarnings\" xml:space=\"preserve\">\n\t  <value>必須包含消息和至少一位用戶</value>\n  </data>\n  <data name=\"PostEmpty\" xml:space=\"preserve\">\n    <value>主題不能為空白</value>\n  </data>\n  <data name=\"PostWait\" xml:space=\"preserve\">\n    <value>你必須等待{0}秒才能再次發表新的主題</value>\n  </data>\n  <data name=\"Previous\" xml:space=\"preserve\">\n    <value>以前</value>\n  </data>\n  <data name=\"RegisterEmailSubject\" xml:space=\"preserve\">\n    <value>論壇註冊 - {0}</value>\n  </data>\n  <data name=\"RegisterEmailThankYou\" xml:space=\"preserve\">\n    <value>感謝您與{0}註冊{2} {2} {1}</value>\n  </data>\n  <data name=\"RegisterEmailThankYouVerify\" xml:space=\"preserve\">\n    <value>感謝您與{0}註冊。請點擊此連結以確認您的註冊： {5} {5} {1} {5} {5} 如果你不能按照這個連結，請訪問以下頁面，剪切和粘貼的確認代碼： {5} {5}友情連結： {2} {5} 代碼： {3} {5} {5} {4}</value>\n  </data>\n  <data name=\"SettingsSaved\" xml:space=\"preserve\">\n    <value>設定已儲存</value>\n  </data>\n  <data name=\"TodayTime\" xml:space=\"preserve\">\n    <value>今天， {0}</value>\n  </data>\n  <data name=\"TopicNotExist\" xml:space=\"preserve\">\n    <value>該主題不存在</value>\n  </data>\n  <data name=\"YesterdayTime\" xml:space=\"preserve\">\n    <value>昨天， {0}</value>\n  </data>\n  <data name=\"Loading\" xml:space=\"preserve\">\n    <value>載入中</value>\n  </data>\n  <data name=\"Voted\" xml:space=\"preserve\">\n    <value>投票</value>\n  </data>\n  <data name=\"VoteUpPublishMessage\" xml:space=\"preserve\">\n\t  <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; 投票給主題: &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n  </data>\n  <data name=\"ScoringGame\" xml:space=\"preserve\">\n    <value>得分王</value>\n  </data>\n  <data name=\"Awards\" xml:space=\"preserve\">\n    <value>獎項</value>\n  </data>\n  <data name=\"EventDefinitions\" xml:space=\"preserve\">\n    <value>事件定義</value>\n  </data>\n  <data name=\"AwardDefinitions\" xml:space=\"preserve\">\n    <value>獎項定義</value>\n  </data>\n  <data name=\"ManualEvent\" xml:space=\"preserve\">\n    <value>手動事件</value>\n  </data>\n  <data name=\"None\" xml:space=\"preserve\">\n    <value>無</value>\n  </data>\n  <data name=\"ActivityFeed\" xml:space=\"preserve\">\n    <value>活動資訊</value>\n  </data>\n  <data name=\"NewReplyPublishMessage\" xml:space=\"preserve\">\n\t  <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt;的所做的主題在後：&lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n  </data>\n  <data name=\"NewPostPublishMessage\" xml:space=\"preserve\">\n\t  <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt;的開始一個新的話題： &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n  </data>\n  <data name=\"ExternalLogins\" xml:space=\"preserve\">\n    <value>外部登入</value>\n  </data>\n  <data name=\"UseExistingForumAccount\" xml:space=\"preserve\">\n    <value>利用現有的論壇帳號</value>\n  </data>\n  <data name=\"ExpiredLogin\" xml:space=\"preserve\">\n    <value>該登入已過期或無效</value>\n  </data>\n  <data name=\"ExternalLoginsDisabled\" xml:space=\"preserve\">\n    <value>外部登入被禁用</value>\n  </data>\n  <data name=\"NoExternalLoginsRegistered\" xml:space=\"preserve\">\n    <value>你有沒有註冊的外部登入</value>\n  </data>\n  <data name=\"DeletePermanently\" xml:space=\"preserve\">\n    <value>永久删除</value>\n  </data>\n  <data name=\"Preview\" xml:space=\"preserve\">\n    <value>預覽</value>\n  </data>\n  <data name=\"AskAQuestion\" xml:space=\"preserve\">\n    <value>問一個問題</value>\n  </data>\n  <data name=\"Answers\" xml:space=\"preserve\">\n    <value>答案</value>\n  </data>\n  <data name=\"ChooseAnswer\" xml:space=\"preserve\">\n    <value>选择答案</value>\n  </data>\n  <data name=\"PostAnswer\" xml:space=\"preserve\">\n    <value>邮政的答案</value>\n  </data>\n  <data name=\"QuestionAnswered\" xml:space=\"preserve\">\n    <value>&lt;a href=\"{0}\"&gt;{1}&lt;/a&gt; 选择的问题的答案： &lt;a href=\"{2}\"&gt;{3}&lt;/a&gt;</value>\n    <comment>{0} is uri to user, {1} is user name, {2} is uri to post, {3} is topic name</comment>\n  </data>\n  <data name=\"SubmitAnswer\" xml:space=\"preserve\">\n    <value>提交答案</value>\n  </data>\n  <data name=\"AppRestartRequired\" xml:space=\"preserve\">\n    <value>需要重新启动应用程序</value>\n  </data>\n  <data name=\"SearchError\" xml:space=\"preserve\">\n    <value>有一个搜索错误</value>\n  </data>\n  <data name=\"CloseOldTopics\" xml:space=\"preserve\">\n    <value>關閉舊主題</value>\n  </data>\n  <data name=\"Days\" xml:space=\"preserve\">\n    <value>天</value>\n  </data>\n  <data name=\"PrivateForum\" xml:space=\"preserve\">\n    <value>私人論壇</value>\n  </data>\n  <data name=\"AccountNotVerified\" xml:space=\"preserve\">\n    <value>帐号未验证</value>\n  </data>\n  <data name=\"SelectText\" xml:space=\"preserve\">\n\t  <value>选择文本</value>\n  </data>\n  <data name=\"Notifications\" xml:space=\"preserve\">\n\t  <value>通知</value>\n  </data>\n  <data name=\"NewReplyNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt;的所做的主題在後：&lt;b&gt;{1}&lt;/b&gt;\n</value>\n  </data>\n  <data name=\"Award\" xml:space=\"preserve\">\n\t  <value>獎項</value>\n  </data>\n  <data name=\"VoteUpNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; 投票給主題: &lt;b&gt;{1}&lt;/b&gt;</value>\n  </data>\n  <data name=\"QuestionAnsweredNotification\" xml:space=\"preserve\">\n\t  <value>&lt;b&gt;{0}&lt;/b&gt; 选择的问题的答案： &lt;b&gt;{1}&lt;/b&gt;\n</value>\n  </data>\n  <data name=\"AutoFollow\" xml:space=\"preserve\">\n\t  <value>自动关注您回复的主题</value>\n  </data>\n  <data name=\"MarkAllRead\" xml:space=\"preserve\">\n\t  <value>标记所有已读</value>\n  </data>\n  <data name=\"UploadImage\" xml:space=\"preserve\">\n\t  <value>上传图片</value>\n  </data>\n  <data name=\"Ignored\" xml:space=\"preserve\">\n\t  <value>被忽略</value>\n  </data>\n  <data name=\"IgnoreList\" xml:space=\"preserve\">\n\t  <value>忽略列表</value>\n  </data>\n  <data name=\"Ignore\" xml:space=\"preserve\">\n\t  <value>忽略</value>\n  </data>\n</root>"
  },
  {
    "path": "src/PopForums/ScoringGame/AwardCalculator.cs",
    "content": "namespace PopForums.ScoringGame;\n\npublic interface IAwardCalculator\n{\n\tTask QueueCalculation(User user, EventDefinition eventDefinition);\n\tTask ProcessCalculation(string eventDefinitionID, int userID);\n}\n\npublic class AwardCalculator : IAwardCalculator\n{\n\tpublic AwardCalculator(IAwardCalculationQueueRepository awardCalcRepository, IEventDefinitionService eventDefinitionService, IUserRepository userRepository, IErrorLog errorLog, IAwardDefinitionService awardDefinitionService, IUserAwardService userAwardService, IPointLedgerRepository pointLedgerRepository, ITenantService tenantService)\n\t{\n\t\t_awardCalcRepository = awardCalcRepository;\n\t\t_eventDefinitionService = eventDefinitionService;\n\t\t_userRepository = userRepository;\n\t\t_errorLog = errorLog;\n\t\t_awardDefinitionService = awardDefinitionService;\n\t\t_userAwardService = userAwardService;\n\t\t_pointLedgerRepository = pointLedgerRepository;\n\t\t_tenantService = tenantService;\n\t}\n\n\tprivate readonly IAwardCalculationQueueRepository _awardCalcRepository;\n\tprivate readonly IEventDefinitionService _eventDefinitionService;\n\tprivate readonly IUserRepository _userRepository;\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly IAwardDefinitionService _awardDefinitionService;\n\tprivate readonly IUserAwardService _userAwardService;\n\tprivate readonly IPointLedgerRepository _pointLedgerRepository;\n\tprivate readonly ITenantService _tenantService;\n\n\tpublic async Task QueueCalculation(User user, EventDefinition eventDefinition)\n\t{\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tvar payload = new AwardCalculationPayload {EventDefinitionID = eventDefinition.EventDefinitionID, UserID = user.UserID, TenantID = tenantID};\n\t\tawait _awardCalcRepository.Enqueue(payload);\n\t}\n\n\tpublic async Task ProcessCalculation(string eventDefinitionID, int userID)\n\t{\n\t\tvar eventDefinition = await _eventDefinitionService.GetEventDefinition(eventDefinitionID);\n\t\tvar user = await _userRepository.GetUser(userID);\n\t\tif (eventDefinition == null)\n\t\t{\n\t\t\t_errorLog.Log(new Exception($\"Event calculation attempt on nonexistent event \\\"{eventDefinitionID}\\\"\"), ErrorSeverity.Warning);\n\t\t\treturn;\n\t\t}\n\t\tif (user == null)\n\t\t{\n\t\t\t_errorLog.Log(new Exception($\"Event calculation attempt on nonexistent user {userID}\"), ErrorSeverity.Warning);\n\t\t\treturn;\n\t\t}\n\t\tvar associatedAwards = await _awardDefinitionService.GetByEventDefinitionID(eventDefinition.EventDefinitionID);\n\t\tforeach (var award in associatedAwards)\n\t\t{\n\t\t\tif (award.IsSingleTimeAward)\n\t\t\t{\n\t\t\t\tvar isAwarded = await _userAwardService.IsAwarded(user, award);\n\t\t\t\tif (isAwarded)\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvar conditions = await _awardDefinitionService.GetConditions(award.AwardDefinitionID);\n\t\t\tvar conditionsMet = 0;\n\t\t\tforeach (var condition in conditions)\n\t\t\t{\n\t\t\t\tvar eventCount = await _pointLedgerRepository.GetEntryCount(user.UserID, condition.EventDefinitionID);\n\t\t\t\tif (eventCount >= condition.EventCount)\n\t\t\t\t\tconditionsMet++;\n\t\t\t}\n\t\t\tif (conditions.Count != 0 && conditionsMet == conditions.Count)\n\t\t\t\tawait _userAwardService.IssueAward(user, award);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/AwardCalculatorWorker.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic interface IAwardCalculatorWorker\n{\n\tvoid Execute();\n}\n\npublic class AwardCalculatorWorker(IAwardCalculator calculator, IAwardCalculationQueueRepository awardCalculationQueueRepository, IErrorLog errorLog) : IAwardCalculatorWorker\n{\n\tpublic async void Execute()\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar nextItem = await awardCalculationQueueRepository.Dequeue();\n\t\t\tif (string.IsNullOrEmpty(nextItem.Key))\n\t\t\t\treturn;\n\t\t\tawait calculator.ProcessCalculation(nextItem.Key, nextItem.Value);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\terrorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/AwardCondition.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic class AwardCondition\n{\n\tpublic string AwardDefinitionID { get; set; }\n\tpublic string EventDefinitionID { get; set; }\n\tpublic int EventCount { get; set; }\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/AwardDefinition.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic class AwardDefinition\n{\n\tpublic string AwardDefinitionID { get; set; }\n\tpublic string Title { get; set; }\n\tpublic string Description { get; set; }\n\tpublic bool IsSingleTimeAward { get; set; }\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/AwardDefinitionService.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic interface IAwardDefinitionService\n{\n\tTask<AwardDefinition> Get(string awardDefinitionID);\n\tTask<List<AwardDefinition>> GetByEventDefinitionID(string eventDefinitionID);\n\tTask Create(AwardDefinition awardDefinition);\n\tTask Delete(string awardDefinitionID);\n\tTask<List<AwardCondition>> GetConditions(string awardDefinitionID);\n\tTask SaveConditions(AwardDefinition awardDefinition, List<AwardCondition> conditions);\n\tTask<List<AwardDefinition>> GetAll();\n\tTask DeleteCondition(string awardDefinitionID, string eventDefinitionID);\n\tTask AddCondition(AwardCondition awardDefintion);\n}\n\npublic class AwardDefinitionService : IAwardDefinitionService\n{\n\tpublic AwardDefinitionService(IAwardDefinitionRepository awardDefintionRepository, IAwardConditionRepository awardConditionRepository)\n\t{\n\t\t_awardDefinitionRepository = awardDefintionRepository;\n\t\t_awardConditionRepository = awardConditionRepository;\n\t}\n\n\tprivate readonly IAwardDefinitionRepository _awardDefinitionRepository;\n\tprivate readonly IAwardConditionRepository _awardConditionRepository;\n\n\tpublic async Task<AwardDefinition> Get(string awardDefinitionID)\n\t{\n\t\treturn await _awardDefinitionRepository.Get(awardDefinitionID);\n\t}\n\n\tpublic async Task<List<AwardDefinition>> GetAll()\n\t{\n\t\treturn await _awardDefinitionRepository.GetAll();\n\t}\n\n\tpublic async Task<List<AwardDefinition>> GetByEventDefinitionID(string eventDefinitionID)\n\t{\n\t\treturn await _awardDefinitionRepository.GetByEventDefinitionID(eventDefinitionID);\n\t}\n\n\tpublic async Task Create(AwardDefinition awardDefinition)\n\t{\n\t\tawait _awardDefinitionRepository.Create(awardDefinition.AwardDefinitionID, awardDefinition.Title, awardDefinition.Description, awardDefinition.IsSingleTimeAward);\n\t}\n\n\tpublic async Task Delete(string awardDefinitionID)\n\t{\n\t\tawait _awardDefinitionRepository.Delete(awardDefinitionID);\n\t}\n\n\tpublic async Task<List<AwardCondition>> GetConditions(string awardDefinitionID)\n\t{\n\t\treturn await _awardConditionRepository.GetConditions(awardDefinitionID);\n\t}\n\n\tpublic async Task SaveConditions(AwardDefinition awardDefinition, List<AwardCondition> conditions)\n\t{\n\t\tawait _awardConditionRepository.DeleteConditions(awardDefinition.AwardDefinitionID);\n\t\tforeach (var condition in conditions)\n\t\t\tcondition.AwardDefinitionID = awardDefinition.AwardDefinitionID;\n\t\tawait _awardConditionRepository.SaveConditions(conditions);\n\t}\n\n\tpublic async Task DeleteCondition(string awardDefinitionID, string eventDefinitionID)\n\t{\n\t\tawait _awardConditionRepository.DeleteCondition(awardDefinitionID, eventDefinitionID);\n\t}\n\n\tpublic async Task AddCondition(AwardCondition awardDefintion)\n\t{\n\t\tawait _awardConditionRepository.SaveConditions(new List<AwardCondition> {awardDefintion});\n\t}\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/EventDefinition.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic class EventDefinition\n{\n\tpublic string EventDefinitionID { get; set; }\n\tpublic string Description { get; set; }\n\tpublic int PointValue { get; set; }\n\tpublic bool IsPublishedToFeed { get; set; }\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/EventDefinitionService.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic interface IEventDefinitionService\n{\n\tTask<EventDefinition> GetEventDefinition(string eventDefinitionID);\n\tTask<List<EventDefinition>> GetAll();\n\tTask Create(EventDefinition eventDefinition);\n\tTask Delete(string eventDefinitionID);\n}\n\npublic class EventDefinitionService : IEventDefinitionService\n{\n\tpublic static Dictionary<string, EventDefinition> StaticEvents = new Dictionary<string, EventDefinition>\n\t{\n\t\t{StaticEventIDs.PostVote, new EventDefinition {EventDefinitionID = StaticEventIDs.PostVote, Description = \"Post vote\", PointValue = 1, IsPublishedToFeed = true}},\n\t\t{StaticEventIDs.PostVoteUndo, new EventDefinition {EventDefinitionID = StaticEventIDs.PostVoteUndo, Description = \"Post vote undo\", PointValue = -1, IsPublishedToFeed = true}},\n\t\t{StaticEventIDs.NewPost, new EventDefinition {EventDefinitionID = StaticEventIDs.NewPost, Description = \"New post\", PointValue = 0, IsPublishedToFeed = true}},\n\t\t{StaticEventIDs.NewTopic, new EventDefinition {EventDefinitionID = StaticEventIDs.NewTopic, Description = \"New topic\", PointValue = 0, IsPublishedToFeed = true}},\n\t\t{StaticEventIDs.QuestionAnswered, new EventDefinition {EventDefinitionID = StaticEventIDs.QuestionAnswered, Description = \"Question answered\", PointValue = 10, IsPublishedToFeed = true}}\n\t};\n\n\tpublic static class StaticEventIDs\n\t{\n\t\tpublic static string PostVote = \"PostVote\";\n\t\tpublic static string PostVoteUndo = \"PostVoteUndo\";\n\t\tpublic static string NewPost = \"NewPost\";\n\t\tpublic static string NewTopic = \"NewTopic\";\n\t\tpublic static string QuestionAnswered = \"QuestionAnswered\";\n\t}\n\n\tpublic EventDefinitionService(IEventDefinitionRepository eventDefinitionRepository, IAwardConditionRepository awardConditionRepository)\n\t{\n\t\t_eventDefinitionRepository = eventDefinitionRepository;\n\t\t_awardConditionRepository = awardConditionRepository;\n\t}\n\n\tprivate readonly IEventDefinitionRepository _eventDefinitionRepository;\n\tprivate readonly IAwardConditionRepository _awardConditionRepository;\n\n\tpublic async Task<EventDefinition> GetEventDefinition(string eventDefinitionID)\n\t{\n\t\tif (StaticEvents.ContainsKey(eventDefinitionID))\n\t\t\treturn StaticEvents[eventDefinitionID];\n\t\treturn await _eventDefinitionRepository.Get(eventDefinitionID);\n\t}\n\n\tpublic async Task<List<EventDefinition>> GetAll()\n\t{\n\t\tvar merged = StaticEvents.Select(x => x.Value).ToList();\n\t\tmerged.AddRange(await _eventDefinitionRepository.GetAll());\n\t\treturn merged.OrderBy(x => x.EventDefinitionID).ToList();\n\t}\n\n\tpublic async Task Create(EventDefinition eventDefinition)\n\t{\n\t\tawait _eventDefinitionRepository.Create(eventDefinition);\n\t}\n\n\tpublic async Task Delete(string eventDefinitionID)\n\t{\n\t\tawait _awardConditionRepository.DeleteConditionsByEventDefinitionID(eventDefinitionID);\n\t\t_eventDefinitionRepository.Delete(eventDefinitionID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/EventPublisher.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic interface IEventPublisher\n{\n\tTask ProcessEvent(string feedMessage, User user, string eventDefinitionID, bool overridePublishToActivityFeed);\n\tTask ProcessManualEvent(string feedMessage, User user, int pointValue);\n}\n\npublic class EventPublisher : IEventPublisher\n{\n\tpublic EventPublisher(IEventDefinitionService eventDefinitionService, IPointLedgerRepository pointLedgerRepository, IFeedService feedService, IAwardCalculator awardCalculator, IProfileService profileService)\n\t{\n\t\t_eventDefinitionService = eventDefinitionService;\n\t\t_pointLedgerRepository = pointLedgerRepository;\n\t\t_feedService = feedService;\n\t\t_awardCalculator = awardCalculator;\n\t\t_profileService = profileService;\n\t}\n\n\tprivate readonly IEventDefinitionService _eventDefinitionService;\n\tprivate readonly IPointLedgerRepository _pointLedgerRepository;\n\tprivate readonly IFeedService _feedService;\n\tprivate readonly IAwardCalculator _awardCalculator;\n\tprivate readonly IProfileService _profileService;\n\n\tpublic async Task ProcessEvent(string feedMessage, User user, string eventDefinitionID, bool overridePublishToActivityFeed)\n\t{\n\t\tvar timeStamp = DateTime.UtcNow;\n\t\tvar eventDefinition = await _eventDefinitionService.GetEventDefinition(eventDefinitionID);\n\t\tvar ledgerEntry = new PointLedgerEntry { UserID = user.UserID, EventDefinitionID = eventDefinitionID, Points = eventDefinition.PointValue, TimeStamp = timeStamp };\n\t\tawait _pointLedgerRepository.RecordEntry(ledgerEntry);\n\t\tawait _profileService.UpdatePointTotal(user);\n\t\tif (eventDefinition.IsPublishedToFeed && !overridePublishToActivityFeed)\n\t\t{\n\t\t\tawait _feedService.PublishToFeed(user, feedMessage, eventDefinition.PointValue, timeStamp);\n\t\t}\n\t\tawait _awardCalculator.QueueCalculation(user, eventDefinition);\n\t}\n\n\tpublic async Task ProcessManualEvent(string feedMessage, User user, int pointValue)\n\t{\n\t\tvar timeStamp = DateTime.UtcNow;\n\t\tvar eventDefinition = new EventDefinition { EventDefinitionID = \"Manual\", PointValue = pointValue };\n\t\tvar ledgerEntry = new PointLedgerEntry { UserID = user.UserID, EventDefinitionID = eventDefinition.EventDefinitionID, Points = eventDefinition.PointValue, TimeStamp = timeStamp };\n\t\tawait _pointLedgerRepository.RecordEntry(ledgerEntry);\n\t\tawait _profileService.UpdatePointTotal(user);\n\t\tawait _feedService.PublishToFeed(user, feedMessage, eventDefinition.PointValue, timeStamp);\n\t}\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/PointLedgerEntry.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic class PointLedgerEntry\n{\n\tpublic int UserID { get; set; }\n\tpublic string EventDefinitionID { get; set; }\n\tpublic int Points { get; set; }\n\tpublic DateTime TimeStamp { get; set; }\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/UserAward.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic class UserAward\n{\n\tpublic int UserAwardID { get; set; }\n\tpublic int UserID { get; set; }\n\tpublic string AwardDefinitionID { get; set; }\n\tpublic string Title { get; set; }\n\tpublic string Description { get; set; }\n\tpublic DateTime TimeStamp { get; set; }\n}"
  },
  {
    "path": "src/PopForums/ScoringGame/UserAwardService.cs",
    "content": "﻿namespace PopForums.ScoringGame;\n\npublic interface IUserAwardService\n{\n\tTask IssueAward(User user, AwardDefinition awardDefinition);\n\tTask<bool> IsAwarded(User user, AwardDefinition awardDefinition);\n\tTask<List<UserAward>> GetAwards(User user);\n}\n\npublic class UserAwardService : IUserAwardService\n{\n\tpublic UserAwardService(IUserAwardRepository userAwardRepository, INotificationTunnel notificationTunnel, ITenantService tenantService)\n\t{\n\t\t_userAwardRepository = userAwardRepository;\n\t\t_notificationTunnel = notificationTunnel;\n\t\t_tenantService = tenantService;\n\t}\n\n\tprivate readonly IUserAwardRepository _userAwardRepository;\n\tprivate readonly INotificationTunnel _notificationTunnel;\n\tprivate readonly ITenantService _tenantService;\n\n\tpublic async Task IssueAward(User user, AwardDefinition awardDefinition)\n\t{\n\t\tawait _userAwardRepository.IssueAward(user.UserID, awardDefinition.AwardDefinitionID, awardDefinition.Title, awardDefinition.Description, DateTime.UtcNow);\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\t_notificationTunnel.SendNotificationForUserAward(awardDefinition.Title, user.UserID, tenantID);\n\t}\n\n\tpublic async Task<bool> IsAwarded(User user, AwardDefinition awardDefinition)\n\t{\n\t\treturn await _userAwardRepository.IsAwarded(user.UserID, awardDefinition.AwardDefinitionID);\n\t}\n\n\tpublic async Task<List<UserAward>> GetAwards(User user)\n\t{\n\t\treturn await _userAwardRepository.GetAwards(user.UserID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/BanService.cs",
    "content": "namespace PopForums.Services;\n\npublic interface IBanService\n{\n\tTask BanIP(string ip);\n\tTask RemoveIPBan(string ip);\n\tTask<List<string>> GetIPBans();\n\tTask BanEmail(string email);\n\tTask RemoveEmailBan(string email);\n\tTask<List<string>> GetEmailBans();\n}\n\npublic class BanService : IBanService\n{\n\tpublic BanService(IBanRepository banRepsoitory)\n\t{\n\t\t_banRepository = banRepsoitory;\n\t}\n\n\tprivate readonly IBanRepository _banRepository;\n\n\tpublic async Task BanIP(string ip)\n\t{\n\t\tawait _banRepository.BanIP(ip.Trim());\n\t}\n\n\tpublic async Task RemoveIPBan(string ip)\n\t{\n\t\tawait _banRepository.RemoveIPBan(ip);\n\t}\n\n\tpublic async Task<List<string>> GetIPBans()\n\t{\n\t\treturn await _banRepository.GetIPBans();\n\t}\n\n\tpublic async Task BanEmail(string email)\n\t{\n\t\tawait _banRepository.BanEmail(email.Trim());\n\t}\n\n\tpublic async Task RemoveEmailBan(string email)\n\t{\n\t\tawait _banRepository.RemoveEmailBan(email);\n\t}\n\n\tpublic async Task<List<string>> GetEmailBans()\n\t{\n\t\treturn await _banRepository.GetEmailBans();\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/CategoryService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ICategoryService\n{\n\tTask<Category> Get(int categoryID);\n\tTask<List<Category>> GetAll();\n\tTask<Category> Create(string title);\n\tTask Delete(int categoryID);\n\tTask Delete(Category category);\n\tTask UpdateTitle(int categoryID, string newTitle);\n\tTask UpdateTitle(Category category, string newTitle);\n\tTask MoveCategoryUp(int categoryID);\n\tTask MoveCategoryDown(int categoryID);\n\tTask MoveCategoryUp(Category category);\n\tTask MoveCategoryDown(Category category);\n}\n\npublic class CategoryService : ICategoryService\n{\n\tpublic CategoryService(ICategoryRepository categoryRepository, IForumRepository forumRepository)\n\t{\n\t\t_categoryRepository = categoryRepository;\n\t\t_forumRepository = forumRepository;\n\t}\n\n\tprivate readonly ICategoryRepository _categoryRepository;\n\tprivate readonly IForumRepository _forumRepository;\n\n\tpublic async Task<Category> Get(int categoryID)\n\t{\n\t\treturn await _categoryRepository.Get(categoryID);\n\t}\n\n\tpublic async Task<List<Category>> GetAll()\n\t{\n\t\treturn await _categoryRepository.GetAll();\n\t}\n\n\tpublic async Task<Category> Create(string title)\n\t{\n\t\tvar newCategory = await _categoryRepository.Create(title, -2);\n\t\tawait ChangeCategorySortOrder(null, 0);\n\t\tnewCategory.SortOrder = 0;\n\t\treturn newCategory;\n\t}\n\n\tpublic async Task Delete(int categoryID)\n\t{\n\t\tvar category = await _categoryRepository.Get(categoryID);\n\t\tif (category == null)\n\t\t\tthrow new Exception($\"Category with ID {categoryID} does not exist.\");\n\t\tawait Delete(category);\n\t}\n\n\tpublic async Task Delete(Category category)\n\t{\n\t\tvar forumResult = await _forumRepository.GetAll();\n\t\tvar forums = forumResult.Where(f => f.CategoryID == category.CategoryID);\n\t\tforeach (var forum in forums)\n\t\t\tawait _forumRepository.UpdateCategoryAssociation(forum.ForumID, null);\n\t\tawait _categoryRepository.Delete(category.CategoryID);\n\t}\n\n\tpublic async Task UpdateTitle(int categoryID, string newTitle)\n\t{\n\t\tvar category = await _categoryRepository.Get(categoryID);\n\t\tif (category == null)\n\t\t\tthrow new Exception($\"Category with ID {categoryID} does not exist.\");\n\t\tawait UpdateTitle(category, newTitle);\n\t}\n\n\tpublic async Task UpdateTitle(Category category, string newTitle)\n\t{\n\t\tcategory.Title = newTitle;\n\t\tawait _categoryRepository.Update(category);\n\t}\n\n\tprivate async Task ChangeCategorySortOrder(Category category, int change)\n\t{\n\t\tvar categories = await GetAll();\n\t\tif (category != null)\n\t\t\tcategories.Single(c => c.CategoryID == category.CategoryID).SortOrder += change;\n\t\tvar sorted = categories.OrderBy(c => c.SortOrder).ToList();\n\t\tfor (var i = 0; i < sorted.Count; i++)\n\t\t{\n\t\t\tsorted[i].SortOrder = i * 2;\n\t\t\tawait _categoryRepository.Update(sorted[i]);\n\t\t}\n\t}\n\n\tpublic async Task MoveCategoryUp(int categoryID)\n\t{\n\t\tvar category = await _categoryRepository.Get(categoryID);\n\t\tif (category == null)\n\t\t\tthrow new Exception($\"Can't move CategoryID {categoryID} up because it does not exist.\");\n\t\tawait MoveCategoryUp(category);\n\t}\n\n\tpublic async Task MoveCategoryDown(int categoryID)\n\t{\n\t\tvar category = await _categoryRepository.Get(categoryID);\n\t\tif (category == null)\n\t\t\tthrow new Exception($\"Can't move CategoryID {categoryID} down because it does not exist.\");\n\t\tawait MoveCategoryDown(category);\n\t}\n\n\tpublic async Task MoveCategoryUp(Category category)\n\t{\n\t\tconst int change = -3;\n\t\tawait ChangeCategorySortOrder(category, change);\n\t}\n\n\tpublic async Task MoveCategoryDown(Category category)\n\t{\n\t\tconst int change = 3;\n\t\tawait ChangeCategorySortOrder(category, change);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ClaimsToRoleMapper.cs",
    "content": "using System.Security.Claims;\n\nnamespace PopForums.Services;\n\npublic interface IClaimsToRoleMapper\n{\n\tTask MapRoles(User user, IEnumerable<Claim> claims);\n}\n\npublic class ClaimsToRoleMapper : IClaimsToRoleMapper\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly IRoleRepository _roleRepository;\n\n\tpublic ClaimsToRoleMapper(IConfig config, IRoleRepository roleRepository)\n\t{\n\t\t_config = config;\n\t\t_roleRepository = roleRepository;\n\t}\n\n\tpublic async Task MapRoles(User user, IEnumerable<Claim> claims)\n\t{\n\t\tbool isAdmin = false, isModerator = false;\n\t\tvar claimsList = claims.ToList();\n\t\tvar adminClaims = claimsList.Where(x => x.Type == _config.OAuthAdminClaimType).ToList();\n\t\tif ((string.IsNullOrEmpty(_config.OAuthAdminClaimValue) && adminClaims.Any()) \n\t\t    || adminClaims.Any(x => x.Value == _config.OAuthAdminClaimValue))\n\t\t\tisAdmin = true;\n\t\tvar modClaims = claimsList.Where(x => x.Type == _config.OAuthModeratorClaimType).ToList();\n\t\tif ((string.IsNullOrEmpty(_config.OAuthModeratorClaimValue) && modClaims.Any()) \n\t\t    || modClaims.Any(x => x.Value == _config.OAuthModeratorClaimValue))\n\t\t\tisModerator = true;\n\n\t\tvar newRoles = new List<string>();\n\t\tif (isAdmin)\n\t\t\tnewRoles.Add(PermanentRoles.Admin);\n\t\tif (isModerator)\n\t\t\tnewRoles.Add(PermanentRoles.Moderator);\n\t\tawait _roleRepository.ReplaceUserRoles(user.UserID, newRoles.ToArray());\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/CloseAgedTopicsWorker.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ICloseAgedTopicsWorker\n{\n\tvoid Execute();\n}\n\npublic class CloseAgedTopicsWorker(ITopicService topicService, IErrorLog errorLog) : ICloseAgedTopicsWorker\n{\n\tpublic async void Execute()\n\t{\n\t\ttry\n\t\t{\n\t\t\tawait topicService.CloseAgedTopics();\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\terrorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/FavoriteTopicService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IFavoriteTopicService\n{\n\tTask<Tuple<List<Topic>, PagerContext>> GetTopics(User user, int pageIndex);\n\tTask<bool> IsTopicFavorite(int userID, int topicID);\n\tTask AddFavoriteTopic(User user, Topic topic);\n\tTask RemoveFavoriteTopic(User user, Topic topic);\n}\n\npublic class FavoriteTopicService : IFavoriteTopicService\n{\n\tpublic FavoriteTopicService(ISettingsManager settingsManager, IFavoriteTopicsRepository favoriteTopicRepository)\n\t{\n\t\t_settingsManager = settingsManager;\n\t\t_favoriteTopicRepository = favoriteTopicRepository;\n\t}\n\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IFavoriteTopicsRepository _favoriteTopicRepository;\n\n\tpublic async Task<Tuple<List<Topic>, PagerContext>> GetTopics(User user, int pageIndex)\n\t{\n\t\tvar pageSize = _settingsManager.Current.TopicsPerPage;\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar topics = await _favoriteTopicRepository.GetFavoriteTopics(user.UserID, startRow, pageSize);\n\t\tvar topicCount = await _favoriteTopicRepository.GetFavoriteTopicCount(user.UserID);\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(topicCount) / Convert.ToDouble(pageSize)));\n\t\tvar pagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\treturn Tuple.Create(topics, pagerContext);\n\t}\n\n\tpublic async Task<bool> IsTopicFavorite(int userID, int topicID)\n\t{\n\t\treturn await _favoriteTopicRepository.IsTopicFavorite(userID, topicID);\n\t}\n\n\tpublic async Task AddFavoriteTopic(User user, Topic topic)\n\t{\n\t\tawait _favoriteTopicRepository.AddFavoriteTopic(user.UserID, topic.TopicID);\n\t}\n\n\tpublic async Task RemoveFavoriteTopic(User user, Topic topic)\n\t{\n\t\tawait _favoriteTopicRepository.RemoveFavoriteTopic(user.UserID, topic.TopicID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ForumPermissionService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IForumPermissionService\n{\n\tTask<ForumPermissionContext> GetPermissionContext(Forum forum, User user);\n\tTask<ForumPermissionContext> GetPermissionContext(Forum forum, User user, Topic topic);\n}\n\npublic class ForumPermissionService : IForumPermissionService\n{\n\tprivate readonly IForumRepository _forumRepository;\n\n\tpublic ForumPermissionService(IForumRepository forumRepository)\n\t{\n\t\t_forumRepository = forumRepository;\n\t}\n\n\tpublic async Task<ForumPermissionContext> GetPermissionContext(Forum forum, User user)\n\t{\n\t\treturn await GetPermissionContext(forum, user, null);\n\t}\n\n\tpublic async Task<ForumPermissionContext> GetPermissionContext(Forum forum, User user, Topic topic)\n\t{\n\t\tvar context = new ForumPermissionContext { DenialReason = string.Empty };\n\t\tvar viewRestrictionRoles = await _forumRepository.GetForumViewRoles(forum.ForumID);\n\t\tvar postRestrictionRoles = await _forumRepository.GetForumPostRoles(forum.ForumID);\n\n\t\t// view\n\t\tif (viewRestrictionRoles.Count == 0)\n\t\t\tcontext.UserCanView = true;\n\t\telse\n\t\t{\n\t\t\tcontext.UserCanView = false;\n\t\t\tif (user != null && viewRestrictionRoles.Where(user.IsInRole).Any())\n\t\t\t\tcontext.UserCanView = true;\n\t\t}\n\n\t\t// post\n\t\tif (user == null || !context.UserCanView)\n\t\t{\n\t\t\tcontext.UserCanPost = false;\n\t\t\tcontext.DenialReason = Resources.LoginToPost;\n\t\t}\n\t\telse\n\t\tif (!user.IsApproved)\n\t\t{\n\t\t\tcontext.DenialReason += \"You can't post until you have verified your account. \";\n\t\t\tcontext.UserCanPost = false;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tif (postRestrictionRoles.Count == 0)\n\t\t\t\tcontext.UserCanPost = true;\n\t\t\telse\n\t\t\t{\n\t\t\t\tif (postRestrictionRoles.Where(user.IsInRole).Any())\n\t\t\t\t\tcontext.UserCanPost = true;\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tcontext.DenialReason += Resources.ForumNoPost + \". \";\n\t\t\t\t\tcontext.UserCanPost = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (topic != null && topic.IsClosed)\n\t\t{\n\t\t\tcontext.UserCanPost = false;\n\t\t\tcontext.DenialReason = Resources.Closed + \". \";\n\t\t}\n\n\t\tif (topic != null && topic.IsDeleted)\n\t\t{\n\t\t\tif (user == null || !user.IsInRole(PermanentRoles.Moderator))\n\t\t\t\tcontext.UserCanView = false;\n\t\t\tcontext.DenialReason += \"Topic is deleted. \";\n\t\t}\n\n\t\tif (forum.IsArchived)\n\t\t{\n\t\t\tcontext.UserCanPost = false;\n\t\t\tcontext.DenialReason += Resources.Archived + \". \";\n\t\t}\n\n\t\t// moderate\n\t\tcontext.UserCanModerate = false;\n\t\tif (user != null && (user.IsInRole(PermanentRoles.Admin) || user.IsInRole(PermanentRoles.Moderator)))\n\t\t\tcontext.UserCanModerate = true;\n\n\t\treturn context;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ForumService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IForumService\n{\n\tTask<Forum> Get(int forumID);\n\tTask<Forum> Get(string urlName);\n\tTask<Forum> Create(int? categoryID, string title, string description, bool isVisible, bool isArchived, int sortOrder, string forumAdapterName, bool isQAForum);\n\tTask UpdateLast(Forum forum);\n\tTask UpdateLast(Forum forum, DateTime lastTime, string lastName);\n\tvoid UpdateCounts(Forum forum);\n\tTask<CategorizedForumContainer> GetCategorizedForumContainer();\n\tTask<List<CategoryContainerWithForums>> GetCategoryContainersWithForums();\n\tTask<CategorizedForumContainer> GetCategorizedForumContainerFilteredForUser(User user);\n\tTask<List<int>> GetNonViewableForumIDs(User user);\n\tTask Update(Forum forum, int? categoryID, string title, string description, bool isVisible, bool isArchived, string forumAdapterName, bool isQAForum);\n\tTask MoveForumUp(int forumID);\n\tTask MoveForumDown(int forumID);\n\tTask<List<string>> GetForumPostRoles(Forum forum);\n\tTask<List<string>> GetForumViewRoles(Forum forum);\n\tDictionary<int, string> GetAllForumTitles();\n\tTask<Tuple<List<Topic>, PagerContext>> GetRecentTopics(User user, bool includeDeleted, int pageIndex);\n\tTask<int> GetAggregateTopicCount();\n\tTask<int> GetAggregatePostCount();\n\tTask<List<int>> GetViewableForumIDsFromViewRestrictedForums(User user);\n\tTopicContainerForQA MapTopicContainerForQA(TopicContainer topicContainer);\n\tTask ModifyForumRoles(ModifyForumRolesContainer container);\n}\n\npublic class ForumService : IForumService\n{\n\tpublic ForumService(IForumRepository forumRepository, ITopicRepository topicRepository, ICategoryRepository categoryRepository, ISettingsManager settingsManager, ILastReadService lastReadService)\n\t{\n\t\t_forumRepository = forumRepository;\n\t\t_topicRepository = topicRepository;\n\t\t_categoryRepository = categoryRepository;\n\t\t_settingsManager = settingsManager;\n\t\t_lastReadService = lastReadService;\n\t}\n\n\tprivate readonly IForumRepository _forumRepository;\n\tprivate readonly ITopicRepository _topicRepository;\n\tprivate readonly ICategoryRepository _categoryRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly ILastReadService _lastReadService;\n\n\tpublic async Task<Forum> Get(int forumID)\n\t{\n\t\treturn await _forumRepository.Get(forumID);\n\t}\n\n\tpublic async Task<Forum> Get(string urlName)\n\t{\n\t\treturn await _forumRepository.Get(urlName);\n\t}\n\n\tpublic async Task<Forum> Create(int? categoryID, string title, string description, bool isVisible, bool isArchived, int sortOrder, string forumAdapterName, bool isQAForum)\n\t{\n\t\tvar urlName = title.ToUniqueUrlName(await _forumRepository.GetUrlNamesThatStartWith(title.ToUrlName()));\n\t\tvar forum = await _forumRepository.Create(categoryID, title, description, isVisible, isArchived, sortOrder, urlName, forumAdapterName, isQAForum);\n\t\tforum.UrlName = urlName;\n\t\tvar forums = await _forumRepository.GetAll();\n\t\tawait SortAndUpdateForums(forums.ToList());\n\t\treturn forum;\n\t}\n\n\tpublic async Task Update(Forum forum, int? categoryID, string title, string description, bool isVisible, bool isArchived, string forumAdapterName, bool isQAForum)\n\t{\n\t\tvar urlName = forum.UrlName;\n\t\tif (forum.Title != title)\n\t\t\turlName = title.ToUniqueUrlName(await _forumRepository.GetUrlNamesThatStartWith(title.ToUrlName()));\n\t\tawait _forumRepository.Update(forum.ForumID, categoryID, title, description, isVisible, isArchived, urlName, forumAdapterName, isQAForum);\n\t}\n\n\tpublic async Task UpdateLast(Forum forum)\n\t{\n\t\tvar topic = await _topicRepository.GetLastUpdatedTopic(forum.ForumID);\n\t\tif (topic != null)\n\t\t\tawait UpdateLast(forum, topic.LastPostTime, topic.LastPostName);\n\t\telse\n\t\t\tawait UpdateLast(forum, new DateTime(2000, 1, 1), String.Empty);\n\t}\n\n\tpublic async Task UpdateLast(Forum forum, DateTime lastTime, string lastName)\n\t{\n\t\tawait _forumRepository.UpdateLastTimeAndUser(forum.ForumID, lastTime, lastName);\n\t}\n\n\tpublic void UpdateCounts(Forum forum)\n\t{\n\t\tnew Thread(() =>\n\t\t{\n\t\t\tvar topicCount = _topicRepository.GetTopicCount(forum.ForumID, false).Result;\n\t\t\tvar postCount = _topicRepository.GetPostCount(forum.ForumID, false).Result;\n\t\t\t_forumRepository.UpdateTopicAndPostCounts(forum.ForumID, topicCount, postCount);\n\t\t}).Start();\n\t}\n\n\tpublic async Task<CategorizedForumContainer> GetCategorizedForumContainer()\n\t{\n\t\tvar forums = _forumRepository.GetAll();\n\t\tvar categories = await _categoryRepository.GetAll();\n\t\tvar container = new CategorizedForumContainer(categories, forums.Result);\n\t\tcontainer.ForumTitle = _settingsManager.Current.ForumTitle;\n\t\treturn container;\n\t}\n\n\tpublic async Task<List<CategoryContainerWithForums>> GetCategoryContainersWithForums()\n\t{\n\t\tvar containers = new List<CategoryContainerWithForums>();\n\t\tvar forumResult = await _forumRepository.GetAll();\n\t\tvar forums = forumResult.ToList();\n\t\tvar categories = await _categoryRepository.GetAll();\n\t\tvar orderedCategories = categories.OrderBy(x => x.SortOrder);\n\t\tvar uncategorized = forums.Where(x => !x.CategoryID.HasValue).OrderBy(x => x.SortOrder).ToList();\n\t\tif (uncategorized.Count > 0)\n\t\t\tcontainers.Add(new CategoryContainerWithForums {Category = new Category { Title = Resources.ForumsUncat }, Forums = uncategorized});\n\t\tforeach (var item in orderedCategories)\n\t\t{\n\t\t\tvar filteredForums = forums.Where(x => x.CategoryID == item.CategoryID).OrderBy(x => x.SortOrder);\n\t\t\tcontainers.Add(new CategoryContainerWithForums { Category = item, Forums = filteredForums });\n\t\t}\n\t\treturn containers;\n\t}\n\n\tpublic async Task<List<int>> GetViewableForumIDsFromViewRestrictedForums(User user)\n\t{\n\t\tvar nonViewableForumIDs = await GetNonViewableForumIDs(user);\n\t\tvar forums = await _forumRepository.GetAllVisible();\n\t\tvar noViewRestrictionForums = forums.Where(f => !nonViewableForumIDs.Contains(f.ForumID));\n\t\treturn noViewRestrictionForums.Select(x => x.ForumID).ToList();\n\t}\n\n\tpublic async Task<CategorizedForumContainer> GetCategorizedForumContainerFilteredForUser(User user)\n\t{\n\t\tvar nonViewableForumIDs = await GetNonViewableForumIDs(user);\n\t\tvar unfilteredForums = await _forumRepository.GetAllVisible();\n\t\tvar forums = unfilteredForums.Where(f => !nonViewableForumIDs.Contains(f.ForumID));\n\t\tvar categories = await _categoryRepository.GetAll();\n\t\tvar container = new CategorizedForumContainer(categories, forums);\n\t\tawait _lastReadService.GetForumReadStatus(user, container);\n\t\tcontainer.ForumTitle = _settingsManager.Current.ForumTitle;\n\t\treturn container;\n\t}\n\n\tpublic async Task<List<int>> GetNonViewableForumIDs(User user)\n\t{\n\t\tvar forumsWithRestrictions = await _forumRepository.GetForumViewRestrictionRoleGraph();\n\t\tvar nonViewableForums = new List<int>();\n\t\tforeach (var item in forumsWithRestrictions.Where(f => f.Value.Count > 0))\n\t\t{\n\t\t\tif (user == null)\n\t\t\t\tnonViewableForums.Add(item.Key);\n\t\t\telse if (!user.Roles.Intersect(item.Value).Any())\n\t\t\t\tnonViewableForums.Add(item.Key);\n\t\t}\n\t\treturn nonViewableForums;\n\t}\n\n\tprivate async Task ChangeForumSortOrder(Forum forum, int change)\n\t{\n\t\tvar forums = await _forumRepository.GetForumsInCategory(forum.CategoryID);\n\t\tforums.Single(c => c.ForumID == forum.ForumID).SortOrder += change;\n\t\tawait SortAndUpdateForums(forums);\n\t}\n\n\tprivate async Task SortAndUpdateForums(IEnumerable<Forum> forums)\n\t{\n\t\tvar sorted = forums.OrderBy(f => f.SortOrder).ToList();\n\t\tfor (var i = 0; i < sorted.Count; i++)\n\t\t{\n\t\t\tvar correctedForum = sorted[i];\n\t\t\tcorrectedForum.SortOrder = i * 2;\n\t\t\tawait _forumRepository.UpdateSortOrder(correctedForum.ForumID, correctedForum.SortOrder);\n\t\t}\n\t}\n\n\tpublic async Task MoveForumUp(int forumID)\n\t{\n\t\tvar forum = await _forumRepository.Get(forumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"Forum {forumID} doesn't exist, can't move it up.\");\n\t\tconst int change = -3;\n\t\tawait ChangeForumSortOrder(forum, change);\n\t}\n\n\tpublic async Task MoveForumDown(int forumID)\n\t{\n\t\tvar forum = await _forumRepository.Get(forumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"Forum {forumID} doesn't exist, can't move it down.\");\n\t\tconst int change = 3;\n\t\tawait ChangeForumSortOrder(forum, change);\n\t}\n\n\tpublic async Task<List<string>> GetForumPostRoles(Forum forum)\n\t{\n\t\treturn await _forumRepository.GetForumPostRoles(forum.ForumID);\n\t}\n\n\tpublic async Task<List<string>> GetForumViewRoles(Forum forum)\n\t{\n\t\treturn await _forumRepository.GetForumViewRoles(forum.ForumID);\n\t}\n\n\tpublic Dictionary<int, string> GetAllForumTitles()\n\t{\n\t\treturn _forumRepository.GetAllForumTitles();\n\t}\n\n\tpublic async Task<Tuple<List<Topic>, PagerContext>> GetRecentTopics(User user, bool includeDeleted, int pageIndex)\n\t{\n\t\tvar nonViewableForumIDs = await GetNonViewableForumIDs(user);\n\t\tvar pageSize = _settingsManager.Current.TopicsPerPage;\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar topics = await _topicRepository.Get(includeDeleted, nonViewableForumIDs, startRow, pageSize);\n\t\tvar topicCount = await _topicRepository.GetTopicCount(includeDeleted, nonViewableForumIDs);\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(topicCount) / Convert.ToDouble(pageSize)));\n\t\tvar pagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\treturn Tuple.Create(topics, pagerContext);\n\t}\n\n\tpublic async Task<int> GetAggregateTopicCount()\n\t{\n\t\treturn await _forumRepository.GetAggregateTopicCount();\n\t}\n\n\tpublic async Task<int> GetAggregatePostCount()\n\t{\n\t\treturn await _forumRepository.GetAggregatePostCount();\n\t}\n\n\tpublic TopicContainerForQA MapTopicContainerForQA(TopicContainer topicContainer)\n\t{\n\t\tvar result = new TopicContainerForQA\n\t\t{\n\t\t\tForum = topicContainer.Forum,\n\t\t\tTopic = topicContainer.Topic,\n\t\t\tPosts = topicContainer.Posts,\n\t\t\tPagerContext = topicContainer.PagerContext,\n\t\t\tPermissionContext = topicContainer.PermissionContext,\n\t\t\tSignatures = topicContainer.Signatures,\n\t\t\tAvatars = topicContainer.Avatars,\n\t\t\tVotedPostIDs = topicContainer.VotedPostIDs,\n\t\t\tTopicState = topicContainer.TopicState\n\t\t};\n\t\ttry\n\t\t{\n\t\t\tvar questionPost = result.Posts.Single(x => x.IsFirstInTopic);\n\t\t\tvar questionComments = result.Posts.Where(x => x.ParentPostID == questionPost.PostID).ToList();\n\t\t\tresult.QuestionPostWithComments = new PostWithChildren { Post = questionPost, Children = questionComments, LastReadTime = topicContainer.LastReadTime };\n\t\t}\n\t\tcatch (InvalidOperationException)\n\t\t{\n\t\t\tthrow new InvalidOperationException($\"There is no post marked as FirstInTopic for TopicID {topicContainer.Topic.TopicID}.\");\n\t\t}\n\t\tvar answers = result.Posts.Where(x => !x.IsFirstInTopic && (x.ParentPostID == 0)).OrderByDescending(x => x.Votes).ThenByDescending(x => x.PostTime).ToList();\n\t\tif (topicContainer.Topic.AnswerPostID.HasValue)\n\t\t{\n\t\t\tvar acceptedAnswer = answers.SingleOrDefault(x => x.PostID == topicContainer.Topic.AnswerPostID.Value);\n\t\t\tif (acceptedAnswer != null)\n\t\t\t{\n\t\t\t\tanswers.Remove(acceptedAnswer);\n\t\t\t\tanswers.Insert(0, acceptedAnswer);\n\t\t\t}\n\t\t}\n\t\tresult.AnswersWithComments = new List<PostWithChildren>();\n\t\tforeach (var item in answers)\n\t\t{\n\t\t\tvar comments = result.Posts.Where(x => x.ParentPostID == item.PostID).ToList();\n\t\t\tresult.AnswersWithComments.Add(new PostWithChildren { Post = item, Children = comments, LastReadTime = topicContainer.LastReadTime });\n\t\t}\n\t\treturn result;\n\t}\n\n\tpublic async Task ModifyForumRoles(ModifyForumRolesContainer container)\n\t{\n\t\tvar forum = await Get(container.ForumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"ForumID {container.ForumID} not found.\");\n\t\tswitch (container.ModifyType)\n\t\t{\n\t\t\tcase ModifyForumRolesType.AddPost:\n\t\t\t\tawait _forumRepository.AddPostRole(forum.ForumID, container.Role);\n\t\t\t\tbreak;\n\t\t\tcase ModifyForumRolesType.RemovePost:\n\t\t\t\tawait _forumRepository.RemovePostRole(forum.ForumID, container.Role);\n\t\t\t\tbreak;\n\t\t\tcase ModifyForumRolesType.AddView:\n\t\t\t\tawait _forumRepository.AddViewRole(forum.ForumID, container.Role);\n\t\t\t\tbreak;\n\t\t\tcase ModifyForumRolesType.RemoveView:\n\t\t\t\tawait _forumRepository.RemoveViewRole(forum.ForumID, container.Role);\n\t\t\t\tbreak;\n\t\t\tcase ModifyForumRolesType.RemoveAllPost:\n\t\t\t\tawait _forumRepository.RemoveAllPostRoles(forum.ForumID);\n\t\t\t\tbreak;\n\t\t\tcase ModifyForumRolesType.RemoveAllView:\n\t\t\t\tawait _forumRepository.RemoveAllViewRoles(forum.ForumID);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Exception(\"ModifyForumRoles doesn't know what to do.\");\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/IPHistoryService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IIPHistoryService\n{\n\tTask<List<IPHistoryEvent>> GetHistory(string ip, DateTime start, DateTime end);\n}\n\npublic class IPHistoryService : IIPHistoryService\n{\n\tpublic IPHistoryService(IPostService postService, ISecurityLogService securityLogService)\n\t{\n\t\t_postService = postService;\n\t\t_securityLogService = securityLogService;\n\t}\n\n\tprivate readonly IPostService _postService;\n\tprivate readonly ISecurityLogService _securityLogService;\n\n\tpublic async Task<List<IPHistoryEvent>> GetHistory(string ip, DateTime start, DateTime end)\n\t{\n\t\tvar list = new List<IPHistoryEvent>();\n\t\tlist.AddRange(await _postService.GetIPHistory(ip, start, end));\n\t\tlist.AddRange(await _securityLogService.GetIPHistory(ip, start, end));\n\t\treturn list.OrderBy(i => i.EventTime).ToList();\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ITopicViewCountService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ITopicViewCountService\n{\n\tTask ProcessView(Topic topic);\n\tvoid SetViewedTopic(Topic topic);\n}"
  },
  {
    "path": "src/PopForums/Services/IUserRetrievalShim.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IUserRetrievalShim\n{\n\tUser GetUser();\n\tProfile GetProfile();\n}"
  },
  {
    "path": "src/PopForums/Services/IgnoreService.cs",
    "content": "namespace PopForums.Services;\n\npublic interface IIgnoreService\n{\n\tTask<IList<IgnoreWithName>> GetIgnoreList(int userID);\n\tTask AddIgnore(int userID, int ignoreUserID);\n\tTask DeleteIgnore(int userID, int ignoreUserID);\n\tTask<List<int>> GetIgnoredUserIdsInList(User user, List<Post> posts);\n}\n\npublic class IgnoreService(IIgnoreRepository ignoreRepository) : IIgnoreService\n{\n\tpublic async Task<IList<IgnoreWithName>> GetIgnoreList(int userID)\n\t{\n\t\treturn await ignoreRepository.GetIgnoreList(userID);\n\t}\n\t\n\tpublic async Task AddIgnore(int userID, int ignoreUserID)\n\t{\n\t\tawait ignoreRepository.AddIgnore(userID, ignoreUserID);\n\t}\n\t\n\tpublic async Task DeleteIgnore(int userID, int ignoreUserID)\n\t{\n\t\tawait ignoreRepository.DeleteIgnore(userID, ignoreUserID);\n\t}\n\t\n\tpublic async Task<List<int>> GetIgnoredUserIdsInList(User user, List<Post> posts)\n\t{\n\t\tif (user == null)\n\t\t\treturn new List<int>();\n\t\tvar userIDs = posts.Select(p => p.UserID).Distinct().ToList();\n\t\treturn await ignoreRepository.GetIgnoredUserIdsInList(user.UserID, userIDs);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ImageService.cs",
    "content": "﻿using SixLabors.ImageSharp;\nusing SixLabors.ImageSharp.Formats.Jpeg;\nusing SixLabors.ImageSharp.PixelFormats;\nusing SixLabors.ImageSharp.Processing;\n\nnamespace PopForums.Services;\n\npublic interface IImageService\n{\n\tTask<bool?> IsUserImageApproved(int userImageID);\n\t[Obsolete(\"Use GetAvatarImageStream(int) instead.\")]\n\tTask<byte[]> GetAvatarImageData(int userAvatarID);\n\tTask<IStreamResponse> GetAvatarImageStream(int userAvatarID);\n\t[Obsolete(\"Use GetUserImageStream(int) instead.\")]\n\tTask<byte[]> GetUserImageData(int userImageID);\n\tTask<IStreamResponse> GetUserImageStream(int userImageID);\n\tTask<DateTime?> GetAvatarImageLastModification(int userAvatarID);\n\tTask<DateTime?> GetUserImageLastModifcation(int userImageID);\n\tbyte[] ConstrainResize(byte[] bytes, int maxWidth, int maxHeight, int qualityLevel, bool cropInsteadOfConstrain);\n\tTask<List<UserImage>> GetUnapprovedUserImages();\n\tTask ApproveUserImage(int userImageID);\n\tTask DeleteUserImage(int userImageID);\n\tTask<UserImage> GetUserImage(int userImageID);\n\tTask<UserImageApprovalContainer> GetUnapprovedUserImageContainer();\n}\n\npublic class ImageService : IImageService\n{\n\tpublic ImageService(IUserAvatarRepository userAvatarRepository, IUserImageRepository userImageRepository, IProfileService profileService, IUserRepository userRepository, ISettingsManager settingsManager)\n\t{\n\t\t_userAvatarRepository = userAvatarRepository;\n\t\t_userImageRepository = userImageRepository;\n\t\t_profileService = profileService;\n\t\t_userRepository = userRepository;\n\t\t_settingsManager = settingsManager;\n\t}\n\n\tprivate readonly IUserAvatarRepository _userAvatarRepository;\n\tprivate readonly IUserImageRepository _userImageRepository;\n\tprivate readonly IProfileService _profileService;\n\tprivate readonly IUserRepository _userRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\n\tpublic async Task<bool?> IsUserImageApproved(int userImageID)\n\t{\n\t\treturn await _userImageRepository.IsUserImageApproved(userImageID);\n\t}\n\n\tpublic async Task<UserImage> GetUserImage(int userImageID)\n\t{\n\t\treturn await _userImageRepository.Get(userImageID);\n\t}\n\n\tpublic async Task ApproveUserImage(int userImageID)\n\t{\n\t\tawait _userImageRepository.ApproveUserImage(userImageID);\n\t}\n\n\tpublic async Task DeleteUserImage(int userImageID)\n\t{\n\t\tvar userImage = await _userImageRepository.Get(userImageID);\n\t\tif (userImage != null)\n\t\t\tawait _profileService.SetCurrentImageIDToNull(userImage.UserID);\n\t\tawait _userImageRepository.DeleteUserImage(userImageID);\n\t}\n\n\t[Obsolete(\"Use GetAvatarImageStream(int) instead.\")]\n\tpublic async Task<byte[]> GetAvatarImageData(int userAvatarID)\n\t{\n\t\treturn await _userAvatarRepository.GetImageData(userAvatarID);\n\t}\n\t\n\tpublic async Task<IStreamResponse> GetAvatarImageStream(int userAvatarID)\n\t{\n\t\treturn await _userAvatarRepository.GetImageStream(userAvatarID);\n\t}\n\n\t[Obsolete(\"Use GetUserImageStream(int) instead.\")]\n\tpublic async Task<byte[]> GetUserImageData(int userImageID)\n\t{\n\t\treturn await _userImageRepository.GetImageData(userImageID);\n\t}\n\n\tpublic async Task<IStreamResponse> GetUserImageStream(int userImageID)\n\t{\n\t\treturn await _userImageRepository.GetImageStream(userImageID);\n\t}\n\n\tpublic async Task<List<UserImage>> GetUnapprovedUserImages()\n\t{\n\t\treturn await _userImageRepository.GetUnapprovedUserImages();\n\t}\n\n\tpublic async Task<DateTime?> GetAvatarImageLastModification(int userAvatarID)\n\t{\n\t\treturn await _userAvatarRepository.GetLastModificationDate(userAvatarID);\n\t}\n\n\tpublic async Task<DateTime?> GetUserImageLastModifcation(int userImageID)\n\t{\n\t\treturn await _userImageRepository.GetLastModificationDate(userImageID);\n\t}\n\n\tpublic byte[] ConstrainResize(byte[] bytes, int maxWidth, int maxHeight, int qualityLevel, bool cropInsteadOfConstrain)\n\t{\n\t\tif (bytes == null)\n\t\t\tthrow new Exception(\"Bytes parameter is null.\");\n\t\tusing (var stream = new MemoryStream(bytes))\n\t\tusing (var image = Image.Load<Rgba32>(stream))\n\t\tusing (var output = new MemoryStream())\n\t\t{\n\t\t\tif (image.Height <= maxHeight && image.Width <= maxWidth)\n\t\t\t\treturn bytes;\n\t\t\tvar options = new ResizeOptions\n\t\t\t{\n\t\t\t\tSize = new Size(maxWidth, maxHeight),\n\t\t\t\tMode = cropInsteadOfConstrain ? ResizeMode.Crop : ResizeMode.Max\n\t\t\t};\n\t\t\timage.Mutate(x => x\n\t\t\t\t.Resize(options)\n\t\t\t\t.GaussianSharpen(0.5f));\n\t\t\timage.Save(output, new JpegEncoder { Quality = qualityLevel });\n\t\t\treturn output.ToArray();\n\t\t}\n\t}\n\n\tpublic async Task<UserImageApprovalContainer> GetUnapprovedUserImageContainer()\n\t{\n\t\tvar isNewUserImageApproved = _settingsManager.Current.IsNewUserImageApproved;\n\t\tvar dictionary = new Dictionary<UserImage, User>();\n\t\tvar unapprovedImages = await GetUnapprovedUserImages();\n\t\tvar users = await _userRepository.GetUsersFromIDs(unapprovedImages.Select(i => i.UserID).ToList());\n\t\tvar container = new UserImageApprovalContainer { Unapproved = new List<UserImagePair>(), IsNewUserImageApproved = isNewUserImageApproved };\n\t\tforeach (var image in unapprovedImages)\n\t\t{\n\t\t\tcontainer.Unapproved.Add(new UserImagePair { User = users.Single(u => u.UserID == image.UserID), UserImage = image });\n\t\t\tdictionary.Add(image, users.Single(u => u.UserID == image.UserID));\n\t\t}\n\t\treturn container;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/LastReadService.cs",
    "content": "namespace PopForums.Services;\n\npublic interface ILastReadService\n{\n\tTask MarkForumRead(User user, Forum forum);\n\tTask MarkAllForumsRead(User user);\n\tTask MarkTopicRead(User user, Topic topic);\n\tTask GetForumReadStatus(User user, CategorizedForumContainer container);\n\tTask GetTopicReadStatus(User user, PagedTopicContainer container);\n\tTask<Post> GetFirstUnreadPost(User user, Topic topic);\n\tTask<DateTime?> GetTopicReadStatus(User user, Topic topic);\n\tTask<DateTime?> GetForumReadStatus(User user, Forum forum);\n\tTask<DateTime?> GetLastReadTime(User user, Topic topic);\n}\n\npublic class LastReadService : ILastReadService\n{\n\tpublic LastReadService(ILastReadRepository lastReadRepository, IPostRepository postRepository)\n\t{\n\t\t_lastReadRepository = lastReadRepository;\n\t\t_postRepository = postRepository;\n\t}\n\n\tprivate readonly ILastReadRepository _lastReadRepository;\n\tprivate readonly IPostRepository _postRepository;\n\n\tpublic async Task MarkForumRead(User user, Forum forum)\n\t{\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(\"user\");\n\t\tif (forum == null)\n\t\t\tthrow new ArgumentNullException(\"forum\");\n\t\tawait _lastReadRepository.SetForumRead(user.UserID, forum.ForumID, DateTime.UtcNow);\n\t\tawait _lastReadRepository.DeleteTopicReadsInForum(user.UserID, forum.ForumID);\n\t}\n\n\tpublic async Task MarkAllForumsRead(User user)\n\t{\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(\"user\");\n\t\tawait _lastReadRepository.SetAllForumsRead(user.UserID, DateTime.UtcNow);\n\t\tawait _lastReadRepository.DeleteAllTopicReads(user.UserID);\n\t}\n\n\tpublic async Task MarkTopicRead(User user, Topic topic)\n\t{\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(\"user\");\n\t\tif (topic == null)\n\t\t\tthrow new ArgumentNullException(\"topic\");\n\t\tawait _lastReadRepository.SetTopicRead(user.UserID, topic.TopicID, DateTime.UtcNow);\n\t}\n\n\tpublic async Task GetForumReadStatus(User user, CategorizedForumContainer container)\n\t{\n\t\tDictionary<int, DateTime> lastReads = null;\n\t\tif (user != null)\n\t\t\tlastReads = await _lastReadRepository.GetLastReadTimesForForums(user.UserID);\n\t\tforeach (var forum in container.AllForums)\n\t\t{\n\t\t\tvar status = ReadStatus.NoNewPosts;\n\t\t\tif (lastReads != null && lastReads.ContainsKey(forum.ForumID))\n\t\t\t\tif (forum.LastPostTime > lastReads[forum.ForumID])\n\t\t\t\t\tstatus = ReadStatus.NewPosts;\n\t\t\tif (lastReads != null && !lastReads.ContainsKey(forum.ForumID))\n\t\t\t\tstatus = ReadStatus.NewPosts;\n\t\t\tif (forum.IsArchived)\n\t\t\t\tstatus |= ReadStatus.Closed;\n\t\t\tcontainer.ReadStatusLookup.Add(forum.ForumID, status);\n\t\t}\n\t}\n\n\tpublic async Task<DateTime?> GetTopicReadStatus(User user, Topic topic)\n\t{\n\t\tif (user != null)\n\t\t{\n\t\t\treturn await _lastReadRepository.GetLastReadTimeForTopic(user.UserID, topic.TopicID);\n\t\t}\n\t\treturn null;\n\t}\n\n\tpublic async Task<DateTime?> GetForumReadStatus(User user, Forum forum)\n\t{\n\t\tif (user != null)\n\t\t{\n\t\t\treturn await _lastReadRepository.GetLastReadTimesForForum(user.UserID, forum.ForumID);\n\t\t}\n\t\treturn null;\n\t}\n\n\tpublic async Task GetTopicReadStatus(User user, PagedTopicContainer container)\n\t{\n\t\tDictionary<int, DateTime> lastForumReads = null;\n\t\tDictionary<int, DateTime> lastTopicReads = null;\n\t\tif (user != null)\n\t\t{\n\t\t\tlastForumReads = await _lastReadRepository.GetLastReadTimesForForums(user.UserID);\n\t\t\tlastTopicReads = await _lastReadRepository.GetLastReadTimesForTopics(user.UserID, container.Topics.Select(t => t.TopicID));\n\t\t}\n\t\tforeach (var topic in container.Topics)\n\t\t{\n\t\t\tvar status = new ReadStatus();\n\t\t\tif (topic.IsClosed)\n\t\t\t\tstatus |= ReadStatus.Closed;\n\t\t\telse\n\t\t\t\tstatus |= ReadStatus.Open;\n\t\t\tif (topic.IsPinned)\n\t\t\t\tstatus |= ReadStatus.Pinned;\n\t\t\telse\n\t\t\t\tstatus |= ReadStatus.NotPinned;\n\t\t\tif (lastForumReads == null)\n\t\t\t\tstatus |= ReadStatus.NoNewPosts;\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar lastRead = DateTime.MinValue;\n\t\t\t\tif (lastForumReads.ContainsKey(topic.ForumID))\n\t\t\t\t\tlastRead = lastForumReads[topic.ForumID];\n\t\t\t\tif (lastTopicReads.ContainsKey(topic.TopicID) && lastTopicReads[topic.TopicID] > lastRead)\n\t\t\t\t\tlastRead = lastTopicReads[topic.TopicID];\n\t\t\t\tif (topic.LastPostTime > lastRead)\n\t\t\t\t\tstatus |= ReadStatus.NewPosts;\n\t\t\t\telse\n\t\t\t\t\tstatus |= ReadStatus.NoNewPosts;\n\t\t\t}\n\t\t\tcontainer.ReadStatusLookup.Add(topic.TopicID, status);\n\t\t}\n\t}\n\n\tpublic async Task<Post> GetFirstUnreadPost(User user, Topic topic)\n\t{\n\t\tif (topic == null)\n\t\t\tthrow new ArgumentException(\"Can't use a null topic.\", \"topic\");\n\t\tvar includeDeleted = false;\n\t\tif (user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\tincludeDeleted = true;\n\t\tvar ids = await _postRepository.GetPostIDsWithTimes(topic.TopicID, includeDeleted);\n\t\tvar postIDs\t= ids.Select(d => new { PostID = d.Key, PostTime = d.Value }).ToList();\n\t\tif (user == null)\n\t\t\treturn await _postRepository.Get(postIDs[0].PostID);\n\t\tvar lastRead = await _lastReadRepository.GetLastReadTimeForTopic(user.UserID, topic.TopicID);\n\t\tif (!lastRead.HasValue)\n\t\t\tlastRead = await _lastReadRepository.GetLastReadTimesForForum(user.UserID, topic.ForumID);\n\t\tif (!lastRead.HasValue || !postIDs.Any(p => p.PostTime > lastRead.Value))\n\t\t\treturn await _postRepository.Get(postIDs[0].PostID);\n\t\tvar firstNew = postIDs.First(p => p.PostTime > lastRead.Value);\n\t\treturn await _postRepository.Get(firstNew.PostID);\n\t}\n\n\tpublic async Task<DateTime?> GetLastReadTime(User user, Topic topic)\n\t{\n\t\tvar lastRead = await _lastReadRepository.GetLastReadTimeForTopic(user.UserID, topic.TopicID);\n\t\tif (!lastRead.HasValue)\n\t\t\tlastRead = await _lastReadRepository.GetLastReadTimesForForum(user.UserID, topic.ForumID);\n\t\treturn lastRead;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/MailingListService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IMailingListService\n{\n\tvoid MailUsers(string subject, string body, string htmlBody, Func<User, string> unsubscribeLinkGenerator);\n}\n\npublic class MailingListService : IMailingListService\n{\n\tpublic MailingListService(IUserService userService, IMailingListComposer mailingListComposer, IErrorLog errorLog)\n\t{\n\t\t_userService = userService;\n\t\t_mailingListComposer = mailingListComposer;\n\t\t_errorLog = errorLog;\n\t}\n\n\tprivate readonly IUserService _userService;\n\tprivate readonly IMailingListComposer _mailingListComposer;\n\tprivate readonly IErrorLog _errorLog;\n\tprivate static Thread _mailWorker;\n\n\tpublic void MailUsers(string subject, string body, string htmlBody, Func<User, string> unsubscribeLinkGenerator)\n\t{\n\t\t_mailWorker = new Thread(() =>\n\t\t{\n\t\t\tvar users = _userService.GetSubscribedUsers().Result;\n\t\t\tforeach (var user in users)\n\t\t\t{\n\t\t\t\tvar unsubLink = unsubscribeLinkGenerator(user);\n\t\t\t\ttry\n\t\t\t\t{\n\t\t\t\t\t_mailingListComposer.ComposeAndQueue(user, subject, body, htmlBody, unsubLink);\n\t\t\t\t}\n\t\t\t\tcatch (Exception exc)\n\t\t\t\t{\n\t\t\t\t\t_errorLog.Log(exc, ErrorSeverity.Error, \"UserID: \" + user.UserID);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\t_mailWorker.Start();\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ModerationLogService.cs",
    "content": "namespace PopForums.Services;\n\npublic interface IModerationLogService\n{\n\tTask LogTopic(User user, ModerationType moderationType, Topic topic, Forum forum);\n\tTask LogTopic(User user, ModerationType moderationType, Topic topic, Forum forum, string comment);\n\tTask LogTopic(ModerationType moderationType, int topicID);\n\tTask LogPost(User user, ModerationType moderationType, Post post, string comment, string oldText);\n\tTask<List<ModerationLogEntry>> GetLog(DateTime start, DateTime end);\n\tTask<List<ModerationLogEntry>> GetLog(Topic topic, bool excludePostEntries);\n\tTask<List<ModerationLogEntry>> GetLog(Post post);\n}\n\npublic class ModerationLogService : IModerationLogService\n{\n\tpublic ModerationLogService(IModerationLogRepository moderationLogRepo)\n\t{\n\t\t_moderationLogRepo = moderationLogRepo;\n\t}\n\n\tprivate readonly IModerationLogRepository _moderationLogRepo;\n\n\tpublic async Task LogTopic(User user, ModerationType moderationType, Topic topic, Forum forum)\n\t{\n\t\tawait LogTopic(user, moderationType, topic, forum, String.Empty);\n\t}\n\n\tpublic async Task LogTopic(User user, ModerationType moderationType, Topic topic, Forum forum, string comment)\n\t{\n\t\tawait _moderationLogRepo.Log(DateTime.UtcNow, user.UserID, user.Name, (int) moderationType, forum != null ? forum.ForumID : (int?)null, topic.TopicID, null, comment, string.Empty);\n\t}\n\n\tpublic async Task LogTopic(ModerationType moderationType, int topicID)\n\t{\n\t\tawait _moderationLogRepo.Log(DateTime.UtcNow, 0, \"System\", (int)moderationType, null, topicID, null, string.Empty, string.Empty);\n\t}\n\n\tpublic async Task LogPost(User user, ModerationType moderationType, Post post, string comment, string oldText)\n\t{\n\t\tawait _moderationLogRepo.Log(DateTime.UtcNow, user.UserID, user.Name, (int)moderationType, null, post.TopicID, post.PostID, comment, oldText);\n\t}\n\n\tpublic async Task<List<ModerationLogEntry>> GetLog(DateTime start, DateTime end)\n\t{\n\t\treturn await _moderationLogRepo.GetLog(start, end);\n\t}\n\n\tpublic async Task<List<ModerationLogEntry>> GetLog(Topic topic, bool excludePostEntries)\n\t{\n\t\treturn await _moderationLogRepo.GetLog(topic.TopicID, excludePostEntries);\n\t}\n\n\tpublic async Task<List<ModerationLogEntry>> GetLog(Post post)\n\t{\n\t\treturn await _moderationLogRepo.GetLog(post.PostID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/PostImageCleanupWorker.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IPostImageCleanupWorker\n{\n\tvoid Execute();\n}\n\npublic class PostImageCleanupWorker(IPostImageService postImageService, IErrorLog errorLog) : IPostImageCleanupWorker\n{\n\tpublic async void Execute()\n\t{\n\t\ttry\n\t\t{\n\t\t\tawait postImageService.DeleteOldPostImages();\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\terrorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/PostImageService.cs",
    "content": "﻿using System.Net.Mime;\n\nnamespace PopForums.Services;\n\npublic interface IPostImageService\n{\n\tbool ProcessImageIsOk(byte[] bytes, string contentType);\n\tTask<PostImagePersistPayload> PersistAndGetPayload();\n\tTask<PostImage> GetWithoutData(string id);\n\t[Obsolete(\"Use the combination of GetWithoutData(int) and GetImageStream(int) instead.\")]\n\tTask<PostImage> Get(string id);\n\tTask<IStreamResponse> GetImageStream(string id);\n\tTask DeleteTempRecord(string id);\n\tTask DeleteTempRecords(string[] ids, string fullText);\n\tTask DeleteOldPostImages();\n}\n\npublic class PostImageService : IPostImageService\n{\n\tprivate readonly IImageService _imageService;\n\tprivate readonly IPostImageRepository _postImageRepository;\n\tprivate readonly IPostImageTempRepository _postImageTempRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly ITenantService _tenantService;\n\n\tpublic PostImageService(IImageService imageService, IPostImageRepository postImageRepository, IPostImageTempRepository postImageTempRepository, ISettingsManager settingsManager, ITenantService tenantService)\n\t{\n\t\t_imageService = imageService;\n\t\t_postImageRepository = postImageRepository;\n\t\t_postImageTempRepository = postImageTempRepository;\n\t\t_settingsManager = settingsManager;\n\t\t_tenantService = tenantService;\n\t}\n\n\tprivate byte[] _bytes;\n\tprivate string _contentType;\n\tprivate bool _isOk;\n\n\tprivate const int HoursOldToDelete = 8;\n\n\tpublic bool ProcessImageIsOk(byte[] bytes, string contentType)\n\t{\n\t\t_contentType = contentType;\n\t\t_bytes = bytes;\n\t\tif (bytes.Length > _settingsManager.Current.PostImageMaxkBytes * 1024)\n\t\t{\n\t\t\t_isOk = false;\n\t\t\treturn false;\n\t\t}\n\n\t\tif (contentType != MediaTypeNames.Image.Jpeg \n\t\t    && contentType != MediaTypeNames.Image.Gif\n\t\t    && contentType != \"image/png\")\n\t\t{\n\t\t\t_isOk = false;\n\t\t\treturn false;\n\t\t}\n\n\t\t_bytes = _imageService.ConstrainResize(bytes, _settingsManager.Current.PostImageMaxWidth, _settingsManager.Current.PostImageMaxHeight, 60, false);\n\t\t_isOk = true;\n\t\treturn true;\n\t}\n\n\tpublic async Task<PostImagePersistPayload> PersistAndGetPayload()\n\t{\n\t\tif (_bytes == null || string.IsNullOrWhiteSpace(_contentType))\n\t\t\tthrow new Exception($\"No image processed or missing content type. Call {nameof(ProcessImageIsOk)} first.\");\n\t\tif (!_isOk)\n\t\t\tthrow new Exception($\"You can't persist an image that was not Ok after calling {nameof(ProcessImageIsOk)}.\");\n\t\tvar payload = await _postImageRepository.Persist(_bytes, _contentType);\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tawait _postImageTempRepository.Save(Guid.Parse(payload.ID), DateTime.UtcNow, tenantID);\n\t\treturn payload;\n\t}\n\n\tpublic async Task<PostImage> GetWithoutData(string id)\n\t{\n\t\tvar postImageSansData = await _postImageRepository.GetWithoutData(id);\n\t\treturn postImageSansData;\n\t}\n\n\t[Obsolete(\"Use the combination of GetWithoutData(int) and GetImageStream(int) instead.\")]\n\tpublic async Task<PostImage> Get(string id)\n\t{\n\t\tvar postImageSansData = await _postImageRepository.Get(id);\n\t\treturn postImageSansData;\n\t}\n\n\tpublic async Task<IStreamResponse> GetImageStream(string id)\n\t{\n\t\treturn await _postImageRepository.GetImageStream(id);\n\t}\n\n\tpublic async Task DeleteTempRecord(string id)\n\t{\n\t\tvar guid = Guid.Parse(id);\n\t\tawait _postImageTempRepository.Delete(guid);\n\t}\n\n\tpublic async Task DeleteTempRecords(string[] ids, string fullText)\n\t{\n\t\tif (ids == null || ids.Length == 0)\n\t\t\treturn;\n\t\tvar filtered = new List<string>();\n\t\tforeach (var id in ids)\n\t\t\tif (fullText.Contains(id, StringComparison.OrdinalIgnoreCase))\n\t\t\t\tfiltered.Add(id);\n\n\t\tforeach (var id in filtered)\n\t\t\tawait DeleteTempRecord(id);\n\t}\n\n\tpublic async Task DeleteOldPostImages()\n\t{\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tvar olderThan = DateTime.UtcNow.AddHours(-HoursOldToDelete);\n\t\tvar ids = await _postImageTempRepository.GetOld(olderThan);\n\t\tforeach (var id in ids)\n\t\t{\n\t\t\tawait _postImageRepository.DeletePostImageData(id.ToString(), tenantID);\n\t\t\tawait _postImageTempRepository.Delete(id);\n\t\t}\n\t}\n\n}"
  },
  {
    "path": "src/PopForums/Services/PostMasterService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IPostMasterService\n{\n\tTask<BasicServiceResponse<Post>> EditPost(int postID, PostEdit postEdit, User editingUser, Func<Post, string> redirectLinkGenerator);\n\tTask<BasicServiceResponse<Topic>> PostNewTopic(User user, NewPost newPost, string ip, string userUrl, Func<Topic, string> topicLinkGenerator, Func<Topic, string> redirectLinkGenerator);\n\tTask<BasicServiceResponse<Post>> PostReply(User user, int parentPostID, string ip, bool isFirstInTopic, NewPost newPost, DateTime postTime, Func<Topic, string> topicLinkGenerator, string userUrl, Func<Post, string> postLinkGenerator, Func<Post, string> redirectLinkGenerator);\n}\n\npublic class PostMasterService : IPostMasterService\n{\n\tprivate readonly ITextParsingService _textParsingService;\n\tprivate readonly ITopicRepository _topicRepository;\n\tprivate readonly IPostRepository _postRepository;\n\tprivate readonly IForumRepository _forumRepository;\n\tprivate readonly IProfileRepository _profileRepository;\n\tprivate readonly IEventPublisher _eventPublisher;\n\tprivate readonly IBroker _broker;\n\tprivate readonly ISearchIndexQueueRepository _searchIndexQueueRepository;\n\tprivate readonly ITenantService _tenantService;\n\tprivate readonly ISubscribedTopicsService _subscribedTopicsService;\n\tprivate readonly IModerationLogService _moderationLogService;\n\tprivate readonly IForumPermissionService _forumPermissionService;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly ITopicViewCountService _topicViewCountService;\n\tprivate readonly IPostImageService _postImageService;\n\n\tpublic PostMasterService(ITextParsingService textParsingService, ITopicRepository topicRepository, IPostRepository postRepository, IForumRepository forumRepository, IProfileRepository profileRepository, IEventPublisher eventPublisher, IBroker broker, ISearchIndexQueueRepository searchIndexQueueRepository, ITenantService tenantService, ISubscribedTopicsService subscribedTopicsService, IModerationLogService moderationLogService, IForumPermissionService forumPermissionService, ISettingsManager settingsManager, ITopicViewCountService topicViewCountService, IPostImageService postImageService)\n\t{\n\t\t_textParsingService = textParsingService;\n\t\t_topicRepository = topicRepository;\n\t\t_postRepository = postRepository;\n\t\t_forumRepository = forumRepository;\n\t\t_profileRepository = profileRepository;\n\t\t_eventPublisher = eventPublisher;\n\t\t_broker = broker;\n\t\t_searchIndexQueueRepository = searchIndexQueueRepository;\n\t\t_tenantService = tenantService;\n\t\t_subscribedTopicsService = subscribedTopicsService;\n\t\t_moderationLogService = moderationLogService;\n\t\t_forumPermissionService = forumPermissionService;\n\t\t_settingsManager = settingsManager;\n\t\t_topicViewCountService = topicViewCountService;\n\t\t_postImageService = postImageService;\n\t}\n\n\tpublic async Task<BasicServiceResponse<Topic>> PostNewTopic(User user, NewPost newPost, string ip, string userUrl, Func<Topic, string> topicLinkGenerator, Func<Topic, string> redirectLinkGenerator)\n\t{\n\t\tif (user == null)\n\t\t\treturn GetPostFailMessage(Resources.LoginToPost);\n\t\tvar forum = await _forumRepository.Get(newPost.ItemID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"Forum {newPost.ItemID} not found\");\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user);\n\t\tif (!permissionContext.UserCanView)\n\t\t\treturn GetPostFailMessage(Resources.ForumNoView);\n\t\tif (!permissionContext.UserCanPost)\n\t\t\treturn GetPostFailMessage(Resources.ForumNoPost);\n\t\tnewPost.FullText = newPost.IsPlainText ? _textParsingService.ForumCodeToHtml(newPost.FullText) : _textParsingService.ClientHtmlToHtml(newPost.FullText);\n\t\tif (await IsNewPostDupeOrInTimeLimit(newPost.FullText, user))\n\t\t\treturn GetPostFailMessage(string.Format(Resources.PostWait, _settingsManager.Current.MinimumSecondsBetweenPosts));\n\t\tif (string.IsNullOrWhiteSpace(newPost.FullText) || string.IsNullOrWhiteSpace(newPost.Title))\n\t\t\treturn GetPostFailMessage(Resources.PostEmpty);\n\t\tnewPost.Title = _textParsingService.Censor(newPost.Title);\n\t\tvar urlName = newPost.Title.ToUniqueUrlName(await _topicRepository.GetUrlNamesThatStartWith(newPost.Title.ToUrlName()));\n\t\tvar timeStamp = DateTime.UtcNow;\n\t\tvar topicID = await _topicRepository.Create(forum.ForumID, newPost.Title, 0, 0, user.UserID, user.Name, user.UserID, user.Name, timeStamp, false, false, false, urlName);\n\t\tvar postID = await _postRepository.Create(topicID, 0, ip, true, newPost.IncludeSignature, user.UserID, user.Name, newPost.Title, newPost.FullText, timeStamp, false, user.Name, null, false, 0);\n\t\tawait _forumRepository.UpdateLastTimeAndUser(forum.ForumID, timeStamp, user.Name);\n\t\tawait _forumRepository.IncrementPostAndTopicCount(forum.ForumID);\n\t\tawait _profileRepository.SetLastPostID(user.UserID, postID);\n\t\tvar topic = new Topic { TopicID = topicID, ForumID = forum.ForumID, IsClosed = false, IsDeleted = false, IsPinned = false, LastPostName = user.Name, LastPostTime = timeStamp, LastPostUserID = user.UserID, ReplyCount = 0, StartedByName = user.Name, StartedByUserID = user.UserID, Title = newPost.Title, UrlName = urlName, ViewCount = 0 };\n\t\t// <a href=\"{0}\">{1}</a> started a new topic: <a href=\"{2}\">{3}</a>\n\t\tvar topicLink = topicLinkGenerator(topic);\n\t\tvar message = string.Format(Resources.NewPostPublishMessage, userUrl, HtmlEncoder.Default.Encode(user.Name ?? string.Empty), topicLink, HtmlEncoder.Default.Encode(topic.Title ?? string.Empty));\n\t\tvar forumHasViewRestrictions = _forumRepository.GetForumViewRoles(forum.ForumID).Result.Count > 0;\n\t\tawait _eventPublisher.ProcessEvent(message, user, EventDefinitionService.StaticEventIDs.NewTopic, forumHasViewRestrictions);\n\t\tawait _eventPublisher.ProcessEvent(string.Empty, user, EventDefinitionService.StaticEventIDs.NewPost, true);\n\t\tforum = await _forumRepository.Get(forum.ForumID);\n\t\t_broker.NotifyForumUpdate(forum);\n\t\t_broker.NotifyTopicUpdate(topic, forum, topicLink);\n\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = topic.TopicID, IsForRemoval = false });\n\t\t_topicViewCountService.SetViewedTopic(topic);\n\t\tvar profile = await  _profileRepository.GetProfile(user.UserID);\n\t\tif (profile != null && profile.IsAutoFollowOnReply)\n\t\t\tawait _subscribedTopicsService.AddSubscribedTopic(user.UserID, topicID);\n\t\tawait _postImageService.DeleteTempRecords(newPost.PostImageIDs, newPost.FullText);\n\n\t\tvar redirectLink = redirectLinkGenerator(topic);\n\n\t\treturn new BasicServiceResponse<Topic> {Data = topic, Message = null, Redirect = redirectLink, IsSuccessful = true};\n\t}\n\n\tprivate BasicServiceResponse<Topic> GetPostFailMessage(string message)\n\t{\n\t\treturn new BasicServiceResponse<Topic> {Data = null, Message = message, Redirect = null, IsSuccessful = false};\n\t}\n\n\tprivate BasicServiceResponse<Post> GetReplyFailMessage(string message)\n\t{\n\t\treturn new BasicServiceResponse<Post> { Data = null, Message = message, Redirect = null, IsSuccessful = false };\n\t}\n\n\tpublic async Task<BasicServiceResponse<Post>> PostReply(User user, int parentPostID, string ip, bool isFirstInTopic, NewPost newPost, DateTime postTime, Func<Topic, string> topicLinkGenerator, string userUrl, Func<Post, string> postLinkGenerator, Func<Post, string> redirectLinkGenerator)\n\t{\n\t\tif (user == null)\n\t\t\treturn GetReplyFailMessage(Resources.LoginToPost);\n\t\tvar topic = await _topicRepository.Get(newPost.ItemID);\n\t\tif (topic == null)\n\t\t\treturn GetReplyFailMessage(Resources.TopicNotExist);\n\t\tif (topic.IsClosed)\n\t\t\treturn GetReplyFailMessage(Resources.Closed);\n\t\tvar forum = await _forumRepository.Get(topic.ForumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"That's not good. Trying to reply to a topic orphaned from Forum {topic.ForumID}, which doesn't exist.\");\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user);\n\t\tif (!permissionContext.UserCanView)\n\t\t\treturn GetReplyFailMessage(Resources.ForumNoView);\n\t\tif (!permissionContext.UserCanPost)\n\t\t\treturn GetReplyFailMessage(Resources.ForumNoPost);\n\t\tnewPost.FullText = newPost.IsPlainText ? _textParsingService.ForumCodeToHtml(newPost.FullText) : _textParsingService.ClientHtmlToHtml(newPost.FullText);\n\t\tif (await IsNewPostDupeOrInTimeLimit(newPost.FullText, user))\n\t\t\treturn GetReplyFailMessage(string.Format(Resources.PostWait, _settingsManager.Current.MinimumSecondsBetweenPosts));\n\t\tif (string.IsNullOrEmpty(newPost.FullText))\n\t\t\treturn GetReplyFailMessage(Resources.PostEmpty);\n\t\tif (newPost.ParentPostID != 0)\n\t\t{\n\t\t\tvar parentPost = await _postRepository.Get(newPost.ParentPostID);\n\t\t\tif (parentPost == null || parentPost.TopicID != topic.TopicID)\n\t\t\t\treturn GetReplyFailMessage(\"This reply attempt is being made to a post in another topic\");\n\t\t}\n\t\tnewPost.Title = _textParsingService.Censor(newPost.Title);\n\n\t\tvar postID = await _postRepository.Create(topic.TopicID, parentPostID, ip, isFirstInTopic, newPost.IncludeSignature, user.UserID, user.Name, newPost.Title, newPost.FullText, postTime, false, user.Name, null, false, 0);\n\t\tvar post = new Post\n\t\t{\n\t\t\tPostID = postID,\n\t\t\tFullText = newPost.FullText,\n\t\t\tIP = ip,\n\t\t\tIsDeleted = false,\n\t\t\tIsEdited = false,\n\t\t\tIsFirstInTopic = isFirstInTopic,\n\t\t\tLastEditName = user.Name,\n\t\t\tLastEditTime = null,\n\t\t\tName = user.Name,\n\t\t\tParentPostID = parentPostID,\n\t\t\tPostTime = postTime,\n\t\t\tShowSig = newPost.IncludeSignature,\n\t\t\tTitle = newPost.Title,\n\t\t\tTopicID = topic.TopicID,\n\t\t\tUserID = user.UserID\n\t\t};\n\t\tawait _topicRepository.IncrementReplyCount(topic.TopicID);\n\t\tawait _topicRepository.UpdateLastTimeAndUser(topic.TopicID, user.UserID, user.Name, postTime);\n\t\tawait _forumRepository.UpdateLastTimeAndUser(topic.ForumID, postTime, user.Name);\n\t\tawait _forumRepository.IncrementPostCount(topic.ForumID);\n\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = topic.TopicID });\n\t\tawait _profileRepository.SetLastPostID(user.UserID, postID);\n\t\tvar topicLink = topicLinkGenerator(topic);\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tawait _subscribedTopicsService.NotifySubscribers(topic, user, tenantID);\n\t\t// <a href=\"{0}\">{1}</a> made a post in the topic: <a href=\"{2}\">{3}</a>\n\t\tvar message = string.Format(Resources.NewReplyPublishMessage, userUrl, HtmlEncoder.Default.Encode(user.Name ?? string.Empty), postLinkGenerator(post), HtmlEncoder.Default.Encode(topic.Title ?? string.Empty));\n\t\tvar forumHasViewRestrictions = _forumRepository.GetForumViewRoles(topic.ForumID).Result.Count > 0;\n\t\tawait _eventPublisher.ProcessEvent(message, user, EventDefinitionService.StaticEventIDs.NewPost, forumHasViewRestrictions);\n\t\ttopic = await _topicRepository.Get(topic.TopicID);\n\t\tforum = await _forumRepository.Get(forum.ForumID);\n\t\t_broker.NotifyNewPosts(topic, post.PostID);\n\t\t_broker.NotifyNewPost(topic, post.PostID);\n\t\t_broker.NotifyForumUpdate(forum);\n\t\t_broker.NotifyTopicUpdate(topic, forum, topicLink);\n\t\t_topicViewCountService.SetViewedTopic(topic);\n\t\tif (newPost.CloseOnReply && user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicClose, topic, null);\n\t\t\tawait _topicRepository.CloseTopic(topic.TopicID);\n\t\t}\n\t\tvar redirectLink = redirectLinkGenerator(post);\n\t\tvar profile = await _profileRepository.GetProfile(user.UserID);\n\t\tif (profile.IsAutoFollowOnReply)\n\t\t\tawait _subscribedTopicsService.AddSubscribedTopic(user.UserID, topic.TopicID);\n\t\tawait _postImageService.DeleteTempRecords(newPost.PostImageIDs, newPost.FullText);\n\n\t\treturn new BasicServiceResponse<Post> { Data = post, Message = null, Redirect = redirectLink, IsSuccessful = true };\n\t}\n\n\tpublic async Task<BasicServiceResponse<Post>> EditPost(int postID, PostEdit postEdit, User editingUser, Func<Post, string> redirectLinkGenerator)\n\t{\n\t\tvar censoredNewTitle = _textParsingService.Censor(postEdit.Title);\n\t\tvar post = await _postRepository.Get(postID);\n\t\tif (!editingUser.IsPostEditable(post))\n\t\t\treturn GetReplyFailMessage(Resources.Forbidden);\n\t\tvar oldText = post.FullText;\n\t\tif (post.IsFirstInTopic && post.Title != censoredNewTitle)\n\t\t{\n\t\t\tif (string.IsNullOrEmpty(censoredNewTitle))\n\t\t\t\treturn GetReplyFailMessage(Resources.PostEmpty);\n\t\t\tvar oldTitle = post.Title;\n\t\t\tpost.Title = censoredNewTitle;\n\t\t\tvar topic = await _topicRepository.Get(post.TopicID);\n\t\t\tvar forum = await _forumRepository.Get(topic.ForumID);\n\t\t\tvar urlName = censoredNewTitle.ToUniqueUrlName(await _topicRepository.GetUrlNamesThatStartWith(censoredNewTitle.ToUrlName()));\n\t\t\tawait _topicRepository.UpdateTitleAndForum(topic.TopicID, forum.ForumID, censoredNewTitle, urlName);\n\t\t\tawait _moderationLogService.LogTopic(editingUser, ModerationType.TopicRenamed, topic, forum, $\"Old title: {oldTitle}\");\n\t\t}\n\t\tif (postEdit.IsPlainText)\n\t\t\tpost.FullText = _textParsingService.ForumCodeToHtml(postEdit.FullText);\n\t\telse\n\t\t\tpost.FullText = _textParsingService.ClientHtmlToHtml(postEdit.FullText);\n\t\tif (string.IsNullOrEmpty(postEdit.FullText))\n\t\t\treturn GetReplyFailMessage(Resources.PostEmpty);\n\t\tpost.ShowSig = postEdit.ShowSig;\n\t\tpost.LastEditTime = DateTime.UtcNow;\n\t\tpost.LastEditName = editingUser.Name;\n\t\tpost.IsEdited = true;\n\t\tawait _postRepository.Update(post);\n\t\tawait _moderationLogService.LogPost(editingUser, ModerationType.PostEdit, post, postEdit.Comment, oldText);\n\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = post.TopicID, IsForRemoval = false });\n\t\tvar redirectLink = redirectLinkGenerator(post);\n\t\tawait _postImageService.DeleteTempRecords(postEdit.PostImageIDs, postEdit.FullText);\n\t\treturn new BasicServiceResponse<Post> { Data = post, IsSuccessful = true, Message = string.Empty, Redirect = redirectLink };\n\t}\n\n\tprivate async Task<bool> IsNewPostDupeOrInTimeLimit(string parsedPost, User user)\n\t{\n\t\tvar postID = await _profileRepository.GetLastPostID(user.UserID);\n\t\tif (postID == null)\n\t\t\treturn false;\n\t\tvar lastPost = await _postRepository.Get(postID.Value);\n\t\tif (lastPost == null)\n\t\t\treturn false;\n\t\tvar minimumSeconds = _settingsManager.Current.MinimumSecondsBetweenPosts;\n\t\tif (DateTime.UtcNow.Subtract(lastPost.PostTime).TotalSeconds < minimumSeconds)\n\t\t\treturn true;\n\t\tif (parsedPost == lastPost.FullText)\n\t\t\treturn true;\n\t\treturn false;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/PostService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IPostService\n{\n\tTask<Tuple<List<Post>, PagerContext>> GetPosts(Topic topic, bool includeDeleted, int pageIndex);\n\tTask<Tuple<List<Post>, PagerContext>> GetPosts(Topic topic, int lastLoadedPostID, bool includeDeleted);\n\tTask<List<Post>> GetPosts(Topic topic, bool includeDeleted);\n\tTask<Post> Get(int postID);\n\tTask<Tuple<int, Topic>> GetTopicPageForPost(Post post, bool includeDeleted);\n\tTask<int> GetPostCount(User user);\n\tTask<PostEdit> GetPostForEdit(Post post, User user);\n\tTask Delete(Post post, User user);\n\tTask Undelete(Post post, User user);\n\tTask<string> GetPostForQuote(Post post, User user, bool forcePlainText);\n\tTask<List<IPHistoryEvent>> GetIPHistory(string ip, DateTime start, DateTime end);\n\tTask<int> GetLastPostID(int topicID);\n\tTask<VotePostContainer> GetVoters(Post post);\n\tTask<int> GetVoteCount(Post post);\n\tTask<List<int>> GetVotedPostIDs(User user, List<Post> posts);\n\tstring GenerateParsedTextPreview(string text, bool isPlainText);\n\tTask<Tuple<int, bool>> ToggleVoteReturnCountAndIsVoted(Post post, User user, string userUrl, string topicUrl, string topicTitle);\n}\n\npublic class PostService : IPostService\n{\n\tpublic PostService(IPostRepository postRepository, IProfileRepository profileRepository, ISettingsManager settingsManager, ITopicService topicService, ITextParsingService textParsingService, IModerationLogService moderationLogService, IForumService forumService, IEventPublisher eventPublisher, IUserService userService, ISearchIndexQueueRepository searchIndexQueueRepository, ITenantService tenantService, INotificationAdapter notificationAdapter)\n\t{\n\t\t_postRepository = postRepository;\n\t\t_profileRepository = profileRepository;\n\t\t_settingsManager = settingsManager;\n\t\t_topicService = topicService;\n\t\t_textParsingService = textParsingService;\n\t\t_moderationLogService = moderationLogService;\n\t\t_forumService = forumService;\n\t\t_eventPublisher = eventPublisher;\n\t\t_userService = userService;\n\t\t_searchIndexQueueRepository = searchIndexQueueRepository;\n\t\t_tenantService = tenantService;\n\t\t_notificationAdapter = notificationAdapter;\n\t}\n\n\tprivate readonly IPostRepository _postRepository;\n\tprivate readonly IProfileRepository _profileRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly ITextParsingService _textParsingService;\n\tprivate readonly IModerationLogService _moderationLogService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly IEventPublisher _eventPublisher;\n\tprivate readonly IUserService _userService;\n\tprivate readonly ISearchIndexQueueRepository _searchIndexQueueRepository;\n\tprivate readonly ITenantService _tenantService;\n\tprivate readonly INotificationAdapter _notificationAdapter;\n\n\tpublic async Task<Tuple<List<Post>, PagerContext>> GetPosts(Topic topic, bool includeDeleted, int pageIndex)\n\t{\n\t\tvar pageSize = _settingsManager.Current.PostsPerPage;\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar posts = await _postRepository.Get(topic.TopicID, includeDeleted, startRow, pageSize);\n\t\tint postCount;\n\t\tif (includeDeleted)\n\t\t\tpostCount = await _postRepository.GetReplyCount(topic.TopicID, true);\n\t\telse\n\t\t\tpostCount = topic.ReplyCount + 1;\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(postCount) / Convert.ToDouble(pageSize)));\n\t\tvar pagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\treturn Tuple.Create(posts, pagerContext);\n\t}\n\n\tpublic async Task<Tuple<List<Post>, PagerContext>> GetPosts(Topic topic, int lastLoadedPostID, bool includeDeleted)\n\t{\n\t\tvar allPosts = await _postRepository.Get(topic.TopicID, includeDeleted);\n\t\tvar lastIndex = allPosts.FindIndex(p => p.PostID == lastLoadedPostID);\n\t\tif (lastIndex < 0)\n\t\t\tthrow new Exception($\"PostID {lastLoadedPostID} is not a part of TopicID {topic.TopicID}.\");\n\t\tvar posts = allPosts.Skip(lastIndex + 1).ToList();\n\t\tvar pageSize = _settingsManager.Current.PostsPerPage;\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(allPosts.Count) / Convert.ToDouble(pageSize)));\n\t\tvar pagerContext = new PagerContext { PageCount = totalPages, PageIndex = totalPages, PageSize = pageSize };\n\t\treturn Tuple.Create(posts, pagerContext);\n\t}\n\n\tpublic async Task<List<Post>> GetPosts(Topic topic, bool includeDeleted)\n\t{\n\t\treturn await _postRepository.Get(topic.TopicID, includeDeleted);\n\t}\n\n\tpublic async Task<Post> Get(int postID)\n\t{\n\t\treturn await _postRepository.Get(postID);\n\t}\n\n\tpublic async Task<Tuple<int, Topic>> GetTopicPageForPost(Post post, bool includeDeleted)\n\t{\n\t\tvar topic = await _topicService.Get(post.TopicID);\n\t\tvar ids = await _postRepository.GetPostIDsWithTimes(post.TopicID, includeDeleted);\n\t\tvar postIDs = ids.Select(p => p.Key).ToList();\n\t\tvar index = postIDs.IndexOf(post.PostID);\n\t\tvar pageSize = _settingsManager.Current.PostsPerPage;\n\t\tvar page = Convert.ToInt32(Math.Floor((double)index/pageSize)) + 1;\n\t\treturn Tuple.Create(page, topic);\n\t}\n\n\tpublic async Task<int> GetPostCount(User user)\n\t{\n\t\treturn await _postRepository.GetPostCount(user.UserID);\n\t}\n\n\tpublic async Task<PostEdit> GetPostForEdit(Post post, User user)\n\t{\n\t\tif (post == null)\n\t\t\tthrow new ArgumentNullException(nameof(post));\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(nameof(user));\n\t\tvar profile = await _profileRepository.GetProfile(user.UserID);\n\t\tvar postEdit = new PostEdit(post) { IsPlainText = profile.IsPlainText, IsFirstInTopic = post.IsFirstInTopic };\n\t\tif (profile.IsPlainText)\n\t\t{\n\t\t\tpostEdit.FullText = _textParsingService.HtmlToForumCode(post.FullText);\n\t\t\tpostEdit.IsPlainText = true;\n\t\t}\n\t\telse\n\t\t\tpostEdit.FullText = _textParsingService.HtmlToClientHtml(post.FullText);\n\t\treturn postEdit;\n\t}\n\n\tpublic async Task<string> GetPostForQuote(Post post, User user, bool forcePlainText)\n\t{\n\t\tif (post == null)\n\t\t\tthrow new ArgumentNullException(nameof(post));\n\t\tif (post.IsDeleted)\n\t\t\treturn \"Post not found\";\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(nameof(user));\n\t\tvar profile = await _profileRepository.GetProfile(user.UserID);\n\t\tstring quote;\n\t\tif (profile.IsPlainText || forcePlainText)\n\t\t\tquote = $\"[quote][i]{post.Name} said:[/i]\\r\\n{_textParsingService.HtmlToForumCode(post.FullText)}[/quote]\\r\\n\\r\\n\";\n\t\telse\n\t\t\tquote = $\"<blockquote><i>{post.Name} said:</i><br />{_textParsingService.HtmlToClientHtml(post.FullText)}</blockquote><p> </p>\";\n\t\treturn quote;\n\t}\n\n\tpublic async Task Delete(Post post, User user)\n\t{\n\t\tif (user.UserID == post.UserID || user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tvar topic = await _topicService.Get(post.TopicID);\n\t\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\t\tif (post.IsFirstInTopic)\n\t\t\t\tawait _topicService.DeleteTopic(topic, user);\n\t\t\telse\n\t\t\t{\n\t\t\t\tawait _moderationLogService.LogPost(user, ModerationType.PostDelete, post, String.Empty, String.Empty);\n\t\t\t\tpost.IsDeleted = true;\n\t\t\t\tpost.LastEditTime = DateTime.UtcNow;\n\t\t\t\tpost.LastEditName = user.Name;\n\t\t\t\tpost.IsEdited = true;\n\t\t\t\tawait _postRepository.Update(post);\n\t\t\t\tawait _topicService.RecalculateReplyCount(topic);\n\t\t\t\tawait _topicService.UpdateLast(topic);\n\t\t\t\t_forumService.UpdateCounts(forum);\n\t\t\t\tawait _forumService.UpdateLast(forum);\n\t\t\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = topic.TopicID });\n\t\t\t}\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator or author to delete post.\");\n\t}\n\n\tpublic async Task Undelete(Post post, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tawait _moderationLogService.LogPost(user, ModerationType.PostUndelete, post, String.Empty, String.Empty);\n\t\t\tpost.IsDeleted = false;\n\t\t\tpost.LastEditTime = DateTime.UtcNow;\n\t\t\tpost.LastEditName = user.Name;\n\t\t\tpost.IsEdited = true;\n\t\t\tawait _postRepository.Update(post);\n\t\t\tvar topic = await _topicService.Get(post.TopicID);\n\t\t\tawait _topicService.RecalculateReplyCount(topic);\n\t\t\tawait _topicService.UpdateLast(topic);\n\t\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\t\t_forumService.UpdateCounts(forum);\n\t\t\tawait _forumService.UpdateLast(forum);\n\t\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload {TenantID = _tenantService.GetTenant(), TopicID = topic.TopicID});\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator to undelete post.\");\n\t}\n\n\tpublic async Task<List<IPHistoryEvent>> GetIPHistory(string ip, DateTime start, DateTime end)\n\t{\n\t\treturn await _postRepository.GetIPHistory(ip, start, end);\n\t}\n\n\tpublic async Task<int> GetLastPostID(int topicID)\n\t{\n\t\treturn await _postRepository.GetLastPostID(topicID);\n\t}\n\n\tpublic async Task<VotePostContainer> GetVoters(Post post)\n\t{\n\t\tvar results = await _postRepository.GetVotes(post.PostID);\n\t\tvar filtered = results.Where(x => x.Value != null).ToDictionary(x => x.Key, x => x.Value);\n\t\tvar container = new VotePostContainer\n\t\t{\n\t\t\tPostID = post.PostID,\n\t\t\tVotes = results.Count,\n\t\t\tVoters = filtered\n\t\t};\n\t\treturn container;\n\t}\n\n\tpublic async Task<int> GetVoteCount(Post post)\n\t{\n\t\treturn await _postRepository.GetVoteCount(post.PostID);\n\t}\n\n\tpublic async Task<List<int>> GetVotedPostIDs(User user, List<Post> posts)\n\t{\n\t\tif (user == null)\n\t\t\treturn new List<int>();\n\t\tvar ids = posts.Select(x => x.PostID).ToList();\n\t\treturn await _postRepository.GetVotedPostIDs(user.UserID, ids);\n\t}\n\n\tpublic string GenerateParsedTextPreview(string text, bool isPlainText)\n\t{\n\t\tvar result = isPlainText ? _textParsingService.ForumCodeToHtml(text) : _textParsingService.ClientHtmlToHtml(text);\n\t\treturn result;\n\t}\n\n\tpublic async Task<Tuple<int, bool>> ToggleVoteReturnCountAndIsVoted(Post post, User user, string userUrl, string topicUrl, string topicTitle)\n\t{\n\t\tif (user == null || post == null || post.UserID == user.UserID)\n\t\t\treturn null;\n\t\tvar voters = await _postRepository.GetVotes(post.PostID);\n\t\tvar isVoted = false;\n\t\tif (voters.ContainsKey(user.UserID))\n\t\t{\n\t\t\tawait _postRepository.DeleteVote(post.PostID, user.UserID);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tawait _postRepository.VotePost(post.PostID, user.UserID);\n\t\t\tisVoted = true;\n\t\t}\n\t\tvar votes = await _postRepository.CalculateVoteCount(post.PostID);\n\t\tawait _postRepository.SetVoteCount(post.PostID, votes);\n\t\tvar votedUpUser = await _userService.GetUser(post.UserID);\n\t\tif (votedUpUser != null)\n\t\t{\n\t\t\tif (isVoted)\n\t\t\t{\n\t\t\t\t// <a href=\"{0}\">{1}</a> voted for a post in the topic: <a href=\"{2}\">{3}</a>\n\t\t\t\tvar message = string.Format(Resources.VoteUpPublishMessage, userUrl, HtmlEncoder.Default.Encode(user.Name ?? string.Empty), topicUrl, HtmlEncoder.Default.Encode(topicTitle ?? string.Empty));\n\t\t\t\tawait _eventPublisher.ProcessEvent(message, votedUpUser, EventDefinitionService.StaticEventIDs.PostVote, false);\n\t\t\t\tawait _notificationAdapter.Vote(user.Name, topicTitle, post.PostID, votedUpUser.UserID);\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar message = $\"<a href=\\\"{userUrl}\\\">{HtmlEncoder.Default.Encode(user.Name ?? string.Empty)}</a> -1: <a href=\\\"{topicUrl}\\\">{HtmlEncoder.Default.Encode(topicTitle ?? string.Empty)}</a>\";\n\t\t\t\tawait _eventPublisher.ProcessEvent(message, votedUpUser, EventDefinitionService.StaticEventIDs.PostVoteUndo, false);\n\t\t\t}\n\t\t}\n\t\treturn new Tuple<int, bool>(votes, isVoted);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/PrivateMessageService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IPrivateMessageService\n{\n\tTask<PrivateMessage> Get(int pmID, int userID);\n\tTask<List<PrivateMessagePost>> GetMostRecentPosts(int pmID, DateTime afterDateTime);\n\tTask<List<PrivateMessagePost>> GetPosts(int pmID, DateTime beforeDateTime);\n\tTask<Tuple<List<PrivateMessage>, PagerContext>> GetPrivateMessages(User user, PrivateMessageBoxType boxType, int pageIndex);\n\tTask<int> GetUnreadCount(int userID);\n\tTask<PrivateMessage> Create(string fullText, User user, List<User> toUsers);\n\tTask Reply(PrivateMessage pm, string fullText, User user);\n\tTask<bool> IsUserInPM(int userID, int pmID);\n\tTask MarkPMRead(int userID, int pmID);\n\tTask Archive(User user, PrivateMessage pm);\n\tTask Unarchive(User user, PrivateMessage pm);\n\tTask<int?> GetFirstUnreadPostID(int pmID, DateTime lastViewDate);\n\tTask<List<PrivateMessageUser>> GetUsers(int pmID);\n}\n\npublic class PrivateMessageService : IPrivateMessageService\n{\n\tpublic PrivateMessageService(IPrivateMessageRepository privateMessageRepo, ISettingsManager settingsManager, ITextParsingService textParsingService, IBroker broker)\n\t{\n\t\t_privateMessageRepository = privateMessageRepo;\n\t\t_settingsManager = settingsManager;\n\t\t_textParsingService = textParsingService;\n\t\t_broker = broker;\n\t}\n\n\tprivate readonly IPrivateMessageRepository _privateMessageRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly ITextParsingService _textParsingService;\n\tprivate readonly IBroker _broker;\n\n\tprivate static int _postPageSize = 20;\n\n\tpublic async Task<PrivateMessage> Get(int pmID, int userID)\n\t{\n\t\treturn await _privateMessageRepository.Get(pmID, userID);\n\t}\n\n\tpublic async Task<List<PrivateMessagePost>> GetMostRecentPosts(int pmID, DateTime afterDateTime)\n\t{\n\t\treturn await _privateMessageRepository.GetPosts(pmID, afterDateTime);\n\t}\n\n\tpublic async Task<List<PrivateMessagePost>> GetPosts(int pmID, DateTime beforeDateTime)\n\t{\n\t\treturn await _privateMessageRepository.GetPosts(pmID, beforeDateTime, _postPageSize);\n\t}\n\n\tpublic async Task<Tuple<List<PrivateMessage>, PagerContext>> GetPrivateMessages(User user, PrivateMessageBoxType boxType, int pageIndex)\n\t{\n\t\tvar total = await _privateMessageRepository.GetBoxCount(user.UserID, boxType);\n\t\tvar pageSize = _settingsManager.Current.TopicsPerPage;\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(total) / Convert.ToDouble(pageSize)));\n\t\tvar pagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\tvar messages = await _privateMessageRepository.GetPrivateMessages(user.UserID, boxType, startRow, pageSize);\n\t\treturn Tuple.Create(messages, pagerContext);\n\t}\n\n\tpublic async Task<int> GetUnreadCount(int userID)\n\t{\n\t\treturn await _privateMessageRepository.GetUnreadCount(userID);\n\t}\n\n\tpublic async Task<PrivateMessage> Create(string fullText, User user, List<User> toUsers)\n\t{\n\t\tif (String.IsNullOrWhiteSpace(fullText))\n\t\t\tthrow new ArgumentNullException(nameof(fullText));\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(nameof(user));\n\t\tif (toUsers == null || toUsers.Count == 0)\n\t\t\tthrow new ArgumentException(\"toUsers must include at least one user.\", nameof(toUsers));\n\t\tvar userIDs = toUsers.Select(x => x.UserID).ToList();\n\t\tuserIDs.Add(user.UserID);\n\t\tvar existingPMID = await _privateMessageRepository.GetExistingFromIDs(userIDs);\n\t\tif (existingPMID != 0)\n\t\t{\n\t\t\tvar existingPM = await _privateMessageRepository.Get(existingPMID, user.UserID);\n\t\t\tawait Reply(existingPM, fullText, user);\n\t\t\treturn existingPM;\n\t\t}\n\t\tvar now = DateTime.UtcNow;\n\t\tvar dynamicUserList = toUsers.Select(x => new {x.UserID, x.Name}).ToList();\n\t\tdynamicUserList.Add(new {user.UserID, user.Name});\n\t\tvar serializedUsers = JsonSerializer.SerializeToElement(dynamicUserList, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n\t\tvar pm = new PrivateMessage\n\t\t{\n\t\t\tUsers = serializedUsers,\n\t\t\tLastPostTime = now\n\t\t};\n\t\tpm.PMID = await _privateMessageRepository.CreatePrivateMessage(pm);\n\t\tvar post = new PrivateMessagePost\n\t\t{\n\t\t\tFullText = _textParsingService.ForumCodeToHtml(fullText),\n\t\t\tName = user.Name,\n\t\t\tPMID = pm.PMID,\n\t\t\tPostTime = now,\n\t\t\tUserID = user.UserID\n\t\t};\n\t\tvar lastReadDate = now.AddMinutes(-1);\n\t\tawait _privateMessageRepository.AddPost(post);\n\t\tawait _privateMessageRepository.AddUsers(pm.PMID, new List<int> {user.UserID}, lastReadDate, false);\n\t\tawait _privateMessageRepository.AddUsers(pm.PMID, toUsers.Select(u => u.UserID).ToList(), lastReadDate, false);\n\t\tforeach (var receiver in toUsers)\n\t\t{\n\t\t\tvar receiverPMCount = await _privateMessageRepository.GetUnreadCount(receiver.UserID);\n\t\t\t_broker.NotifyPMCount(receiver.UserID, receiverPMCount);\n\t\t}\n\t\treturn pm;\n\t}\n\n\tpublic async Task Reply(PrivateMessage pm, string fullText, User user)\n\t{\n\t\tif (pm == null || pm.PMID == 0)\n\t\t\tthrow new ArgumentException(\"Can't reply to a PM that hasn't been persisted.\", \"pm\");\n\t\tif (fullText == null)\n\t\t\tthrow new ArgumentNullException(\"fullText\");\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(\"user\");\n\t\tif (await IsUserInPM(user.UserID, pm.PMID) == false)\n\t\t\tthrow new Exception(\"Can't add a PM reply for a user not part of the PM.\");\n\t\tvar post = new PrivateMessagePost\n\t\t{\n\t\t\tFullText = _textParsingService.ForumCodeToHtml(fullText),\n\t\t\tName = user.Name,\n\t\t\tPMID = pm.PMID,\n\t\t\tPostTime = DateTime.UtcNow,\n\t\t\tUserID = user.UserID\n\t\t};\n\t\tawait _privateMessageRepository.AddPost(post);\n\t\tvar users = await _privateMessageRepository.GetUsers(pm.PMID);\n\t\tforeach (var u in users)\n\t\t\tawait _privateMessageRepository.SetArchive(pm.PMID, u.UserID, false);\n\t\tvar now = DateTime.UtcNow;\n\t\tawait _privateMessageRepository.UpdateLastPostTime(pm.PMID, now);\n\t\tawait _privateMessageRepository.SetLastViewTime(pm.PMID, user.UserID, now);\n\t\t_broker.SendPMMessage(post);\n\t\tforeach (var receiver in users)\n\t\t{\n\t\t\tvar receiverPMCount = await _privateMessageRepository.GetUnreadCount(receiver.UserID);\n\t\t\t_broker.NotifyPMCount(receiver.UserID, receiverPMCount);\n\t\t}\n\t}\n\n\tpublic async Task<bool> IsUserInPM(int userID, int pmID)\n\t{\n\t\tvar pmUsers = await _privateMessageRepository.GetUsers(pmID);\n\t\treturn pmUsers.Count(p => p.UserID == userID) != 0;\n\t}\n\n\tpublic async Task MarkPMRead(int userID, int pmID)\n\t{\n\t\tawait _privateMessageRepository.SetLastViewTime(pmID, userID, DateTime.UtcNow);\n\t\tvar pmCount = await _privateMessageRepository.GetUnreadCount(userID);\n\t\t_broker.NotifyPMCount(userID, pmCount);\n\t}\n\n\tpublic async Task Archive(User user, PrivateMessage pm)\n\t{\n\t\tawait _privateMessageRepository.SetArchive(pm.PMID, user.UserID, true);\n\t}\n\n\tpublic async Task Unarchive(User user, PrivateMessage pm)\n\t{\n\t\tawait _privateMessageRepository.SetArchive(pm.PMID, user.UserID, false);\n\t}\n\n\tpublic async Task<int?> GetFirstUnreadPostID(int pmID, DateTime lastViewDate)\n\t{\n\t\treturn await _privateMessageRepository.GetFirstUnreadPostID(pmID, lastViewDate);\n\t}\n\n\tpublic async Task<List<PrivateMessageUser>> GetUsers(int pmID)\n\t{\n\t\treturn await _privateMessageRepository.GetUsers(pmID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ProfileService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IProfileService\n{\n\tTask<Profile> GetProfile(User user);\n\tTask Create(Profile profile);\n\tTask Update(Profile profile);\n\tTask<Profile> GetProfileForEdit(User user, bool forcePlainText = false);\n\tTask EditUserProfile(User user, UserEditProfile userEditProfile);\n\tTask<Dictionary<int, string>> GetSignatures(List<Post> posts);\n\tTask<Dictionary<int, int>> GetAvatars(List<Post> posts);\n\tTask SetCurrentImageIDToNull(int userID);\n\tstring GetUnsubscribeHash(User user);\n\tTask<bool> Unsubscribe(User user, string hash);\n\tTask UpdatePointTotal(User user);\n}\n\npublic class ProfileService : IProfileService\n{\n\tprivate readonly IProfileRepository _profileRepository;\n\tprivate readonly ITextParsingService _textParsingService;\n\tprivate readonly IPointLedgerRepository _pointLedgerRepository;\n\n\tpublic ProfileService(IProfileRepository profileRepository, ITextParsingService textParsingService, IPointLedgerRepository pointLedgerRepository)\n\t{\n\t\t_profileRepository = profileRepository;\n\t\t_textParsingService = textParsingService;\n\t\t_pointLedgerRepository = pointLedgerRepository;\n\t}\n\n\tpublic async Task<Profile> GetProfile(User user)\n\t{\n\t\tif (user == null)\n\t\t\treturn null;\n\t\treturn await _profileRepository.GetProfile(user.UserID);\n\t}\n\n\tpublic async Task<Profile> GetProfileForEdit(User user, bool forcePlainText = false)\n\t{\n\t\tvar profile = await _profileRepository.GetProfile(user.UserID);\n\t\tvar userEditProfile = new Profile();\n\t\tif (string.IsNullOrWhiteSpace(profile.Signature))\n\t\t\tuserEditProfile.Signature = string.Empty;\n\t\telse\n\t\t{\n\t\t\tif (profile.IsPlainText || forcePlainText) \n\t\t\t\tuserEditProfile.Signature = _textParsingService.HtmlToForumCode(profile.Signature);\n\t\t\telse\n\t\t\t\tuserEditProfile.Signature = _textParsingService.HtmlToClientHtml(profile.Signature);\n\t\t}\n\t\tuserEditProfile.IsSubscribed = profile.IsSubscribed;\n\t\tuserEditProfile.ShowDetails = profile.ShowDetails;\n\t\tuserEditProfile.IsPlainText = profile.IsPlainText;\n\t\tuserEditProfile.HideVanity = profile.HideVanity;\n\t\tuserEditProfile.Location = profile.Location;\n\t\tuserEditProfile.Dob = profile.Dob;\n\t\tuserEditProfile.Web = profile.Web;\n\t\tuserEditProfile.Instagram = profile.Instagram;\n\t\tuserEditProfile.Facebook = profile.Facebook;\n\t\tuserEditProfile.IsAutoFollowOnReply = profile.IsAutoFollowOnReply;\n\t\treturn userEditProfile;\n\t}\n\n\tpublic async Task EditUserProfile(User user, UserEditProfile userEditProfile)\n\t{\n\t\tvar profile = await _profileRepository.GetProfile(user.UserID);\n\t\tif (profile == null)\n\t\t\tthrow new Exception($\"No profile found for UserID {user.UserID}\");\n\t\tif (profile.IsPlainText)\n\t\t\tprofile.Signature = _textParsingService.ForumCodeToHtml(userEditProfile.Signature);\n\t\telse\n\t\t\tprofile.Signature = _textParsingService.ClientHtmlToHtml(userEditProfile.Signature);\n\t\tprofile.IsSubscribed = userEditProfile.IsSubscribed;\n\t\tprofile.ShowDetails = userEditProfile.ShowDetails;\n\t\tprofile.IsPlainText = userEditProfile.IsPlainText;\n\t\tprofile.HideVanity = userEditProfile.HideVanity;\n\t\tprofile.Location = userEditProfile.Location;\n\t\tprofile.Dob = userEditProfile.Dob;\n\t\tprofile.Web = userEditProfile.Web;\n\t\tprofile.Instagram = userEditProfile.Instagram;\n\t\tprofile.Facebook = userEditProfile.Facebook;\n\t\tprofile.IsAutoFollowOnReply = userEditProfile.IsAutoFollowOnReply;\n\t\tawait _profileRepository.Update(profile);\n\t}\n\n\tpublic async Task Create(Profile profile)\n\t{\n\t\tif (profile.UserID == 0)\n\t\t\tthrow new Exception(\"Can't create a profile not associated with a valid UserID\");\n\t\tawait _profileRepository.Create(profile);\n\t}\n\n\tpublic async Task Update(Profile profile)\n\t{\n\t\tprofile.Signature = profile.Signature.Trim();\n\t\tif (await _profileRepository.Update(profile) == false)\n\t\t\tthrow new Exception($\"Profile with UserID {profile.UserID} does not exist.\");\n\t}\n\n\tpublic async Task<Dictionary<int, string>> GetSignatures(List<Post> posts)\n\t{\n\t\tvar userIDs = posts.Where(p => p.ShowSig).Select(p => p.UserID).Distinct().ToList();\n\t\treturn await _profileRepository.GetSignatures(userIDs);\n\t}\n\n\tpublic async Task<Dictionary<int, int>> GetAvatars(List<Post> posts)\n\t{\n\t\tvar userIDs = posts.Select(p => p.UserID).Distinct().ToList();\n\t\treturn await _profileRepository.GetAvatars(userIDs);\n\t}\n\n\tpublic async Task SetCurrentImageIDToNull(int userID)\n\t{\n\t\tawait _profileRepository.SetCurrentImageIDToNull(userID);\n\t}\n\n\tpublic string GetUnsubscribeHash(User user)\n\t{\n\t\tvar source = user.Name + user.Email;\n\t\treturn source.GetSHA256Hash().Replace(\"+\", string.Empty).Replace(\"=\", string.Empty);\n\t}\n\n\tpublic async Task<bool> Unsubscribe(User user, string hash)\n\t{\n\t\tvar calculatedHash = GetUnsubscribeHash(user);\n\t\tif (calculatedHash != hash)\n\t\t\treturn false;\n\t\tvar profile = await GetProfile(user);\n\t\tprofile.IsSubscribed = false;\n\t\tawait Update(profile);\n\t\treturn true;\n\t}\n\n\tpublic async Task UpdatePointTotal(User user)\n\t{\n\t\tvar total = await _pointLedgerRepository.GetPointTotal(user.UserID);\n\t\tawait _profileRepository.UpdatePoints(user.UserID, total);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/QueuedEmailService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IQueuedEmailService\n{\n\tTask CreateAndQueueEmail(QueuedEmailMessage queuedEmailMessage);\n}\n\npublic class QueuedEmailService : IQueuedEmailService\n{\n\tprivate readonly IQueuedEmailMessageRepository _queuedEmailMessageRepository;\n\tprivate readonly IEmailQueueRepository _emailQueueRepository;\n\tprivate readonly ITenantService _tenantService;\n\n\tpublic QueuedEmailService(IQueuedEmailMessageRepository queuedEmailMessageRepository, IEmailQueueRepository emailQueueRepository, ITenantService tenantService)\n\t{\n\t\t_queuedEmailMessageRepository = queuedEmailMessageRepository;\n\t\t_emailQueueRepository = emailQueueRepository;\n\t\t_tenantService = tenantService;\n\t}\n\n\tpublic async Task CreateAndQueueEmail(QueuedEmailMessage queuedEmailMessage)\n\t{\n\t\tvar id = await _queuedEmailMessageRepository.CreateMessage(queuedEmailMessage);\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tvar payload = new EmailQueuePayload { MessageID = id, EmailQueuePayloadType = EmailQueuePayloadType.FullMessage, TenantID = tenantID };\n\t\tawait _emailQueueRepository.Enqueue(payload);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ReCaptchaService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IReCaptchaService\n{\n\tTask<ReCaptchaResponse> VerifyToken(string token, string ip);\n}\n\npublic class ReCaptchaService : IReCaptchaService\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly ISecurityLogService _securityLogService;\n\tprivate const string VerifyUrl = \"https://www.google.com/recaptcha/api/siteverify\";\n\n\tpublic ReCaptchaService(IConfig config, ISecurityLogService securityLogService)\n\t{\n\t\t_config = config;\n\t\t_securityLogService = securityLogService;\n\t}\n\n\tpublic async Task<ReCaptchaResponse> VerifyToken(string token, string ip)\n\t{\n\t\tvar values = new Dictionary<string, string>\n\t\t{\n\t\t\t{\"secret\", _config.ReCaptchaSecretKey},\n\t\t\t{\"response\", token},\n\t\t\t{\"ip\", ip}\n\t\t};\n\t\tHttpResponseMessage httpResult;\n\t\tusing (var client = new HttpClient())\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\thttpResult = await client.PostAsync(VerifyUrl, new FormUrlEncodedContent(values));\n\t\t\t}\n\t\t\tcatch (HttpRequestException)\n\t\t\t{\n\t\t\t\treturn new ReCaptchaResponse {IsSuccess = false, ErrorCodes = new[] { \"HttpRequestException\" } };\n\t\t\t}\n\t\t}\n\t\tvar content = await httpResult.Content.ReadAsStringAsync();\n\t\tvar verifyResult = JsonSerializer.Deserialize<ReCaptchaResponse>(content);\n\t\tif (!verifyResult.IsSuccess)\n\t\t\tawait _securityLogService.CreateLogEntry((User) null, null, ip, string.Empty, SecurityLogType.ReCaptchaFailed);\n\t\treturn verifyResult;\n\t}\n}\n\npublic class ReCaptchaResponse\n{\n\t[JsonPropertyName(\"success\")]\n\tpublic bool IsSuccess { get; set; }\n\t[JsonPropertyName(\"challenge_ts\")]\n\tpublic DateTime ChallengeTimeStamp { get; set; }\n\t[JsonPropertyName(\"hostname\")]\n\tpublic string HostName { get; set; }\n\t[JsonPropertyName(\"error-codes\")]\n\tpublic string[] ErrorCodes { get; set; }\n}"
  },
  {
    "path": "src/PopForums/Services/SearchIndexSubsystem.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ISearchIndexSubsystem\n{\n\tvoid DoIndex(int topicID, string tenantID, bool isForRemoval);\n\tvoid RemoveIndex(int topicID, string tenantID);\n}\n\n/// <summary>\n/// Implementation for in-database searching. Does not support multi-tenancy.\n/// </summary>\npublic class SearchIndexSubsystem : ISearchIndexSubsystem\n{\n\tprivate readonly ISearchService _searchService;\n\tprivate readonly IPostService _postService;\n\tprivate readonly ITopicService _topicService;\n\n\tpublic SearchIndexSubsystem(ISearchService searchService, IPostService postService, ITopicService topicService)\n\t{\n\t\t_searchService = searchService;\n\t\t_postService = postService;\n\t\t_topicService = topicService;\n\t}\n\n\tpublic void DoIndex(int topicID, string tenantID, bool isForRemoval)\n\t{\n\t\t_searchService.DeleteAllIndexedWordsForTopic(topicID);\n\t\tif (isForRemoval)\n\t\t\treturn;\n\n\t\tvar topic = _topicService.Get(topicID).Result;\n\t\tif (topic == null)\n\t\t\treturn;\n\t\t\t\n\t\tvar junkList = _searchService.GetJunkWords().Result;\n\t\tvar wordList = new List<SearchWord>();\n\t\tvar alphaNum = SearchService.SearchWordPattern;\n\t\tvar posts = _postService.GetPosts(topic, false).Result;\n\n\t\tforeach (var post in posts)\n\t\t{\n\t\t\tvar firstPostMultiplier = 1;\n\t\t\tif (post.IsFirstInTopic) firstPostMultiplier = 2;\n\t\t\tvar postWords = post.FullText.Split(new[] { \" \", \"\\r\\n\" }, StringSplitOptions.RemoveEmptyEntries);\n\t\t\tif (postWords.Length > 0)\n\t\t\t{\n\t\t\t\tfor (var x = 0; x < postWords.Length; x++)\n\t\t\t\t{\n\t\t\t\t\tforeach (Match match in alphaNum.Matches(postWords[x]))\n\t\t\t\t\t{\n\t\t\t\t\t\tTestForIndex(topic, match.Value, 1, firstPostMultiplier, true, wordList, junkList);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// index the name\n\t\t\tforeach (Match match in alphaNum.Matches(post.Name))\n\t\t\t{\n\t\t\t\tTestForIndex(topic, match.Value, 2, firstPostMultiplier, false, wordList, junkList);\n\t\t\t}\n\t\t}\n\n\t\t// bonus for appearing in title\n\t\tforeach (Match match in alphaNum.Matches(topic.Title))\n\t\t{\n\t\t\tTestForIndex(topic, match.Value, 20, 1, false, wordList, junkList);\n\t\t}\n\n\t\tforeach (var word in wordList)\n\t\t{\n\t\t\t_searchService.SaveSearchWord(word);\n\t\t}\n\t}\n\n\tpublic void RemoveIndex(int topicID, string tenantID)\n\t{\n\t}\n\n\tprivate void TestForIndex(Topic topic, string testWord, int increment, int multiplier, bool cap, List<SearchWord> wordList, List<String> junkList)\n\t{\n\t\ttestWord = testWord.ToLower();\n\t\tif (junkList.IndexOf(testWord) < 0)\n\t\t{\n\t\t\tvar foundWord = wordList.Find(w => w.Word == testWord);\n\t\t\tif (foundWord != null)\n\t\t\t{\n\t\t\t\tfoundWord.Rank += increment * multiplier;\n\t\t\t\t// cap the word frequency score\n\t\t\t\tif (cap && foundWord.Rank > 120) foundWord.Rank = 120;\n\t\t\t}\n\t\t\telse wordList.Add(new SearchWord { Rank = 1, TopicID = topic.TopicID, Word = testWord });\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/SearchIndexWorker.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ISearchIndexWorker\n{\n\tvoid Execute();\n}\n\npublic class SearchIndexWorker(IErrorLog errorLog, ISearchIndexSubsystem searchIndexSubsystem, ISearchService searchService) : ISearchIndexWorker\n{\n\tpublic async void Execute()\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar payload = await searchService.GetNextTopicForIndexing();\n\t\t\tif (payload == null)\n\t\t\t\treturn;\n\t\t\tsearchIndexSubsystem.DoIndex(payload.TopicID, payload.TenantID, payload.IsForRemoval);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\terrorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/SearchService.cs",
    "content": "namespace PopForums.Services;\n\npublic interface ISearchService\n{\n\tTask<List<string>> GetJunkWords();\n\tTask CreateJunkWord(string word);\n\tTask DeleteJunkWord(string word);\n\tTask<Tuple<Response<List<Topic>>, PagerContext>> GetTopics(string searchTerm, SearchType searchType, User user, bool includeDeleted, int pageIndex);\n\tTask<SearchIndexPayload> GetNextTopicForIndexing();\n\tTask DeleteAllIndexedWordsForTopic(int topicID);\n\tTask SaveSearchWord(SearchWord searchWord);\n}\n\npublic class SearchService : ISearchService\n{\n\tpublic SearchService(ISearchRepository searchRepository, ISettingsManager settingsManager, IForumService forumService, ISearchIndexQueueRepository searchIndexQueueRepository, IErrorLog errorLog)\n\t{\n\t\t_searchRepository = searchRepository;\n\t\t_settingsManager = settingsManager;\n\t\t_forumService = forumService;\n\t\t_searchIndexQueueRepository = searchIndexQueueRepository;\n\t\t_errorLog = errorLog;\n\t}\n\n\tprivate readonly ISearchRepository _searchRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly ISearchIndexQueueRepository _searchIndexQueueRepository;\n\tprivate readonly IErrorLog _errorLog;\n\n\tpublic static Regex SearchWordPattern = new Regex(@\"[\\w'\\@\\#\\$\\%\\^\\&\\*]{2,}\", RegexOptions.None);\n\n\tpublic async Task<Tuple<Response<List<Topic>>, PagerContext>> GetTopics(string searchTerm, SearchType searchType, User user, bool includeDeleted, int pageIndex)\n\t{\n\t\tvar nonViewableForumIDs = await _forumService.GetNonViewableForumIDs(user);\n\t\tvar pageSize = _settingsManager.Current.TopicsPerPage;\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar topicCount = 0;\n\t\tResponse<List<Topic>> topics;\n\t\tPagerContext pagerContext;\n\t\tif (string.IsNullOrEmpty(searchTerm))\n\t\t\ttopics = new Response<List<Topic>>(new List<Topic>(), true);\n\t\telse\n\t\t{\n\t\t\t(topics, topicCount) = await _searchRepository.SearchTopics(searchTerm, nonViewableForumIDs, searchType, startRow, pageSize);\n\t\t}\n\t\tif (topics.IsValid)\n\t\t{\n\t\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(topicCount) / Convert.ToDouble(pageSize)));\n\t\t\tpagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\t}\n\t\telse\n\t\t{\n\t\t\ttopics = new Response<List<Topic>>(new List<Topic>(), false);\n\t\t\tpagerContext = new PagerContext {PageCount = 1, PageIndex = 1, PageSize = 1};\n\t\t\tvar exc = new Exception($\"Search service error: {topics.Exception?.Message}\");\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Warning, topics.DebugInfo);\n\t\t}\n\t\treturn Tuple.Create(topics, pagerContext);\n\t}\n\n\tpublic async Task<SearchIndexPayload> GetNextTopicForIndexing()\n\t{\n\t\tvar payload = await _searchIndexQueueRepository.Dequeue();\n\t\treturn payload;\n\t}\n\n\tpublic async Task<List<string>> GetJunkWords()\n\t{\n\t\treturn await _searchRepository.GetJunkWords();\n\t}\n\n\tpublic async Task CreateJunkWord(string word)\n\t{\n\t\tawait _searchRepository.CreateJunkWord(word);\n\t}\n\n\tpublic async Task DeleteJunkWord(string word)\n\t{\n\t\tawait _searchRepository.DeleteJunkWord(word);\n\t}\n\n\tpublic async Task DeleteAllIndexedWordsForTopic(int topicID)\n\t{\n\t\tawait _searchRepository.DeleteAllIndexedWordsForTopic(topicID);\n\t}\n\n\tpublic async Task SaveSearchWord(SearchWord searchWord)\n\t{\n\t\tawait _searchRepository.SaveSearchWord(searchWord.TopicID, searchWord.Word, searchWord.Rank);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/SecurityLogService.cs",
    "content": "namespace PopForums.Services;\n\npublic interface ISecurityLogService\n{\n\tTask<List<SecurityLogEntry>> GetLogEntriesByUserID(int userID, DateTime startDate, DateTime endDate);\n\tTask<List<SecurityLogEntry>> GetLogEntriesByUserName(string name, DateTime startDate, DateTime endDate);\n\tTask CreateLogEntry(User user, User targetUser, string ip, string message, SecurityLogType securityLogType);\n\tTask CreateLogEntry(int? userID, int? targetUserID, string ip, string message, SecurityLogType securityLogType);\n\tTask CreateLogEntry(int? userID, int? targetUserID, string ip, string message, SecurityLogType securityLogType, DateTime timeStamp);\n\tTask<List<IPHistoryEvent>> GetIPHistory(string ip, DateTime start, DateTime end);\n}\n\npublic class SecurityLogService : ISecurityLogService\n{\n\tpublic SecurityLogService(ISecurityLogRepository securityLogRepsoitory, IUserRepository userRepository)\n\t{\n\t\t_securityLogRepository = securityLogRepsoitory;\n\t\t_userRepository = userRepository;\n\t}\n\n\tprivate readonly ISecurityLogRepository _securityLogRepository;\n\tprivate readonly IUserRepository _userRepository;\n\n\tpublic async Task<List<SecurityLogEntry>> GetLogEntriesByUserID(int userID, DateTime startDate, DateTime endDate)\n\t{\n\t\treturn await _securityLogRepository.GetByUserID(userID, startDate, endDate);\n\t}\n\n\tpublic async Task<List<IPHistoryEvent>> GetIPHistory(string ip, DateTime start, DateTime end)\n\t{\n\t\treturn await _securityLogRepository.GetIPHistory(ip, start, end);\n\t}\n\n\tpublic async Task<List<SecurityLogEntry>> GetLogEntriesByUserName(string name, DateTime startDate, DateTime endDate)\n\t{\n\t\tvar user = await _userRepository.GetUserByName(name);\n\t\tif (user == null)\n\t\t\treturn new List<SecurityLogEntry>();\n\t\treturn await _securityLogRepository.GetByUserID(user.UserID, startDate, endDate);\n\t}\n\n\tpublic async Task CreateLogEntry(User user, User targetUser, string ip, string message, SecurityLogType securityLogType)\n\t{\n\t\tif (!string.IsNullOrEmpty(message) && message.Length > 255)\n\t\t\tmessage = message.Substring(0, 255);\n\t\tawait CreateLogEntry(user?.UserID, targetUser?.UserID, ip, message, securityLogType);\n\t}\n\n\tpublic async Task CreateLogEntry(int? userID, int? targetUserID, string ip, string message, SecurityLogType securityLogType)\n\t{\n\t\tawait CreateLogEntry(userID, targetUserID, ip, message, securityLogType, DateTime.UtcNow);\n\t}\n\n\tpublic async Task CreateLogEntry(int? userID, int? targetUserID, string ip, string message, SecurityLogType securityLogType, DateTime timeStamp)\n\t{\n\t\tif (ip == null)\n\t\t\tthrow new ArgumentNullException(\"ip\");\n\t\tif (message == null)\n\t\t\tthrow new ArgumentNullException(\"message\");\n\t\tvar entry = new SecurityLogEntry\n\t\t{\n\t\t\tUserID = userID,\n\t\t\tTargetUserID = targetUserID,\n\t\t\tActivityDate = timeStamp,\n\t\t\tIP = ip,\n\t\t\tMessage = message,\n\t\t\tSecurityLogType = securityLogType\n\t\t};\n\t\tawait _securityLogRepository.Create(entry);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/ServiceHeartbeatService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IServiceHeartbeatService\n{\n\tTask RecordHeartbeat(string serviceName, string machineName);\n\tTask<List<ServiceHeartbeat>> GetAll();\n\tTask ClearAll();\n}\n\npublic class ServiceHeartbeatService : IServiceHeartbeatService\n{\n\tprivate readonly IServiceHeartbeatRepository _serviceHeartbeatRepository;\n\n\tpublic ServiceHeartbeatService(IServiceHeartbeatRepository serviceHeartbeatRepository)\n\t{\n\t\t_serviceHeartbeatRepository = serviceHeartbeatRepository;\n\t}\n\n\tpublic async Task RecordHeartbeat(string serviceName, string machineName)\n\t{\n\t\tawait _serviceHeartbeatRepository.RecordHeartbeat(serviceName, machineName, DateTime.UtcNow);\n\t}\n\n\tpublic async Task<List<ServiceHeartbeat>> GetAll()\n\t{\n\t\treturn await _serviceHeartbeatRepository.GetAll();\n\t}\n\n\tpublic async Task ClearAll()\n\t{\n\t\tawait _serviceHeartbeatRepository.ClearAll();\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/SetupService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ISetupService\n{\n\tbool IsRuntimeConnectionAndSetupGood();\n\tbool IsConnectionPossible();\n\tbool IsDatabaseSetup();\n\tTask<Tuple<User, Exception>> SetupDatabase(SetupVariables setupVariables);\n\tException SetupDatabaseWithoutSettingsOrUser();\n}\n\npublic class SetupService : ISetupService\n{\n\tpublic SetupService(ISetupRepository setupRepository, IUserService userService, ISettingsManager settingsManager, IProfileService profileService)\n\t{\n\t\t_setupRepository = setupRepository;\n\t\t_userService = userService;\n\t\t_settingsManager = settingsManager;\n\t\t_profileService = profileService;\n\t}\n\n\tprivate readonly ISetupRepository _setupRepository;\n\tprivate readonly IUserService _userService;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IProfileService _profileService;\n\n\tprivate static bool _isConnectionSetupGood;\n\n\tprivate static readonly object _locker = new();\n\n\tpublic bool IsRuntimeConnectionAndSetupGood()\n\t{\n\t\tif (_isConnectionSetupGood)\n\t\t\treturn true;\n\t\tlock (_locker)\n\t\t{\n\t\t\tvar canConnect = _setupRepository.IsConnectionPossible();\n\t\t\tvar isSetup = _setupRepository.IsDatabaseSetup();\n\t\t\t_isConnectionSetupGood = canConnect && isSetup;\n\t\t}\n\t\treturn _isConnectionSetupGood;\n\t}\n\n\tpublic bool IsConnectionPossible()\n\t{\n\t\treturn _setupRepository.IsConnectionPossible();\n\t}\n\n\tpublic bool IsDatabaseSetup()\n\t{\n\t\treturn _setupRepository.IsDatabaseSetup();\n\t}\n\n\tpublic Exception SetupDatabaseWithoutSettingsOrUser()\n\t{\n\t\ttry\n\t\t{\n\t\t\t_setupRepository.SetupDatabase();\n\t\t}\n\t\tcatch (Exception exception)\n\t\t{\n\t\t\treturn exception;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tpublic async Task<Tuple<User, Exception>> SetupDatabase(SetupVariables setupVariables)\n\t{\n\t\tException exception = null;\n\t\ttry\n\t\t{\n\t\t\t_setupRepository.SetupDatabase();\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\texception = exc;\n\t\t\treturn Tuple.Create<User, Exception>(null, exception);\n\t\t}\n\n\t\tvar settings = _settingsManager.Current;\n\t\tsettings.ForumTitle = setupVariables.ForumTitle;\n\t\tsettings.SmtpServer = setupVariables.SmtpServer;\n\t\tsettings.SmtpPort = setupVariables.SmtpPort;\n\t\tsettings.MailerAddress = setupVariables.MailerAddress;\n\t\tsettings.UseSslSmtp = setupVariables.UseSslSmtp;\n\t\tsettings.UseEsmtp = setupVariables.UseEsmtp;\n\t\tsettings.SmtpUser = setupVariables.SmtpUser;\n\t\tsettings.SmtpPassword = setupVariables.SmtpPassword;\n\t\t_settingsManager.SaveCurrent();\n\n\t\tvar user = await _userService.CreateUser(setupVariables.Name, setupVariables.Email, setupVariables.Password, true, \"\");\n\t\tuser.Roles = new List<string> {PermanentRoles.Admin, PermanentRoles.Moderator};\n\t\tvar profile = new Profile { UserID = user.UserID, IsTos = true, IsSubscribed = true, ShowDetails = true, IsAutoFollowOnReply = true };\n\t\tawait _profileService.Create(profile);\n\t\tvar edit = new UserEdit(user, profile);\n\t\tawait _userService.EditUser(user, edit, false, false, null, null, \"\", user);\n\t\t//PopForumsActivation.StartServicesIfRunningInstance();\n\t\treturn Tuple.Create(user, exception);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/SitemapService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ISitemapService\n{\n\tTask<string> GenerateIndex(Func<int, string> pageLinkGenerator);\n\tTask<int> GetSitemapPageCount();\n\tTask<string> GeneratePage(Func<string, string> topicLinkGenerator, int page);\n}\n\npublic class SitemapService : ISitemapService\n{\n\tprivate readonly ITopicRepository _topicRepository;\n\tprivate readonly IForumRepository _forumRepository;\n\n\tprivate const double _pageSize = 30000;\n\n\tpublic SitemapService(ITopicRepository topicRepository, IForumRepository forumRepository)\n\t{\n\t\t_topicRepository = topicRepository;\n\t\t_forumRepository = forumRepository;\n\t}\n\n\tpublic async Task<string> GenerateIndex(Func<int, string> pageLinkGenerator)\n\t{\n\t\tvar pageCount = await GetSitemapPageCount();\n\t\tvar s = new StringBuilder(@\"<?xml version=\"\"1.0\"\" encoding=\"\"UTF-8\"\"?>\n<sitemapindex xmlns=\"\"http://www.sitemaps.org/schemas/sitemap/0.9\"\">\n\");\n\t\tfor (int p = 0; p < pageCount; p++)\n\t\t{\n\t\t\ts.Append(\"\\t<sitemap>\\r\\n\\t\\t<loc>\");\n\t\t\ts.Append(pageLinkGenerator(p));\n\t\t\ts.Append(\"</loc>\\r\\n\\t</sitemap>\\r\\n\");\n\t\t}\n\t\ts.Append(\"</sitemapindex>\");\n\t\tvar result = s.ToString();\n\t\treturn result;\n\t}\n\n\tpublic async Task<string> GeneratePage(Func<string, string> topicLinkGenerator, int page)\n\t{\n\t\tvar nonViewableForumGraph = await _forumRepository.GetForumViewRestrictionRoleGraph();\n\t\t// any forum with a role attached isn't viewable, shouldn't appear in sitemap\n\t\tvar nonViewableForumIDs = nonViewableForumGraph.Where(x => x.Value.Count > 0).Select(x => x.Key).ToList();\n\t\tvar startRow = page == 0 ? 1 : (page * (int)_pageSize) + 1;\n\t\tvar namesAndDates = await _topicRepository.GetUrlNames(false, nonViewableForumIDs, startRow, (int)_pageSize);\n\t\tvar s = new StringBuilder(@\"<?xml version=\"\"1.0\"\" encoding=\"\"UTF-8\"\"?>\n<urlset xmlns=\"\"http://www.sitemaps.org/schemas/sitemap/0.9\"\">\n\");\n\t\tforeach (var item in namesAndDates)\n\t\t{\n\t\t\ts.Append(\"\\t<url>\\r\\n\\t\\t<loc>\");\n\t\t\ts.Append(topicLinkGenerator(item.Item1));\n\t\t\ts.Append(\"</loc>\\r\\n\\t\\t<lastmod>\");\n\t\t\ts.Append(item.Item2.ToString(\"yyyy-MM-ddThh:mmzzz\"));\n\t\t\ts.Append(\"</lastmod>\\r\\n\\t\\t<changefreq>daily</changefreq>\\r\\n\\t</url>\\r\\n\");\n\t\t}\n\t\ts.Append(\"</urlset>\");\n\t\tvar result = s.ToString();\n\t\treturn result;\n\t}\n\n\tpublic async Task<int> GetSitemapPageCount()\n\t{\n\t\tvar nonViewableForumGraph = await _forumRepository.GetForumViewRestrictionRoleGraph();\n\t\t// any forum with a role attached isn't viewable, shouldn't appear in sitemap\n\t\tvar nonViewableForumIDs = nonViewableForumGraph.Where(x => x.Value.Count > 0).Select(x => x.Key).ToList();\n\t\tvar topicCount = await _topicRepository.GetTopicCount(false, nonViewableForumIDs);\n\t\tif (topicCount < _pageSize)\n\t\t\treturn 1;\n\t\tvar result = Math.Ceiling(topicCount / _pageSize);\n\t\treturn Convert.ToInt32(result);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/SubscribeNotificationWorker.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ISubscribeNotificationWorker\n{\n\tvoid Execute();\n}\n\npublic class SubscribeNotificationWorker(ISubscribeNotificationRepository subscribeNotificationRepository, ISubscribedTopicsService subscribedTopicsService, INotificationAdapter notificationAdapter, IErrorLog errorLog) : ISubscribeNotificationWorker\n{\n\tpublic async void Execute()\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar payload = await subscribeNotificationRepository.Dequeue();\n\t\t\tif (payload == null)\n\t\t\t\treturn;\n\t\t\tvar userIDs = await subscribedTopicsService.GetSubscribedUserIDs(payload.TopicID);\n\t\t\tvar filteredUserIDs = userIDs.Where(x => x != payload.PostingUserID);\n\t\t\tforeach (var userID in filteredUserIDs)\n\t\t\t\tawait notificationAdapter.Reply(payload.PostingUserName, payload.TopicTitle, payload.TopicID, userID, payload.TenantID);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\terrorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/SubscribedTopicsService.cs",
    "content": "﻿using PopForums.Models;\n\nnamespace PopForums.Services;\n\npublic interface ISubscribedTopicsService\n{\n\tTask AddSubscribedTopic(int userID, int topicID);\n\tTask RemoveSubscribedTopic(User user, Topic topic);\n\tTask TryRemoveSubscribedTopic(User user, Topic topic);\n\tTask NotifySubscribers(Topic topic, User postingUser, string tenantID);\n\tTask<Tuple<List<Topic>, PagerContext>> GetTopics(User user, int pageIndex);\n\tTask<bool> IsTopicSubscribed(int userID, int topicID);\n\tTask<List<int>> GetSubscribedUserIDs(int topicID);\n}\n\npublic class SubscribedTopicsService : ISubscribedTopicsService\n{\n\tpublic SubscribedTopicsService(ISubscribedTopicsRepository subscribedTopicsRepository, ISettingsManager settingsManager, INotificationAdapter notificationAdapter, ISubscribeNotificationRepository subscribeNotificationRepository)\n\t{\n\t\t_subscribedTopicsRepository = subscribedTopicsRepository;\n\t\t_settingsManager = settingsManager;\n\t\t_notificationAdapter = notificationAdapter;\n\t\t_subscribeNotificationRepository = subscribeNotificationRepository;\n\t}\n\n\tprivate readonly ISubscribedTopicsRepository _subscribedTopicsRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly INotificationAdapter _notificationAdapter;\n\tprivate readonly ISubscribeNotificationRepository _subscribeNotificationRepository;\n\n\tpublic async Task AddSubscribedTopic(int userID, int topicID)\n\t{\n\t\tvar isSubscribed = await _subscribedTopicsRepository.IsTopicSubscribed(userID, topicID);\n\t\tif (!isSubscribed)\n\t\t\tawait _subscribedTopicsRepository.AddSubscribedTopic(userID, topicID);\n\t}\n\n\tpublic async Task RemoveSubscribedTopic(User user, Topic topic)\n\t{\n\t\tawait _subscribedTopicsRepository.RemoveSubscribedTopic(user.UserID, topic.TopicID);\n\t}\n\n\tpublic async Task TryRemoveSubscribedTopic(User user, Topic topic)\n\t{\n\t\tif (user != null && topic != null)\n\t\t\tawait RemoveSubscribedTopic(user, topic);\n\t}\n\t\n\tpublic async Task NotifySubscribers(Topic topic, User postingUser, string tenantID)\n\t{\n\t\tvar payload = new SubscribeNotificationPayload\n\t\t{\n\t\t\tTopicID = topic.TopicID,\n\t\t\tTopicTitle = topic.Title,\n\t\t\tPostingUserID = postingUser.UserID,\n\t\t\tPostingUserName = postingUser.Name,\n\t\t\tTenantID = tenantID\n\t\t};\n\t\tawait _subscribeNotificationRepository.Enqueue(payload);\n\t}\n\n\tpublic async Task<Tuple<List<Topic>, PagerContext>> GetTopics(User user, int pageIndex)\n\t{\n\t\tvar pageSize = _settingsManager.Current.TopicsPerPage;\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar topics = await _subscribedTopicsRepository.GetSubscribedTopics(user.UserID, startRow, pageSize);\n\t\tvar topicCount = await _subscribedTopicsRepository.GetSubscribedTopicCount(user.UserID);\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(topicCount) / Convert.ToDouble(pageSize)));\n\t\tvar pagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\treturn Tuple.Create(topics, pagerContext);\n\t}\n\n\tpublic async Task<bool> IsTopicSubscribed(int userID, int topicID)\n\t{\n\t\treturn await _subscribedTopicsRepository.IsTopicSubscribed(userID, topicID);\n\t}\n\n\tpublic async Task<List<int>> GetSubscribedUserIDs(int topicID)\n\t{\n\t\treturn await _subscribedTopicsRepository.GetSubscribedUserIDs(topicID);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/TenantService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ITenantService\n{\n\tvoid SetTenant(string tenantID);\n\tstring GetTenant();\n}\n\npublic class TenantService : ITenantService\n{\n\tpublic void SetTenant(string tenantID)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic string GetTenant()\n\t{\n\t\treturn string.Empty;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/TextParsingService.cs",
    "content": "namespace PopForums.Services;\r\n\r\npublic interface ITextParsingService\r\n{\r\n\t/// <summary>\r\n\t/// Converts forum code from the browser to HTML for storage. This method wraps <see cref=\"TextParsingService.CleanForumCode\"/> and <see cref=\"TextParsingService.ForumCodeToHtml\"/>.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tstring ForumCodeToHtml(string text);\r\n\r\n\t/// <summary>\r\n\t/// Converts client HTML from the browser to HTML for storage. This method wraps <see cref=\"TextParsingService.ClientHtmlToForumCode\"/> and <see cref=\"TextParsingService.ForumCodeToHtml\"/>.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tstring ClientHtmlToHtml(string text);\r\n\r\n\tstring HtmlToClientHtml(string text);\r\n\tstring Censor(string text);\r\n\r\n\t/// <summary>\r\n\t/// Converts client HTML to forum code. Important: This method does NOT attempt to create valid HTML, as it assumes that the forum code \r\n\t/// will be cleaned. This method should generally not be called directly except for testing.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tstring ClientHtmlToForumCode(string text);\r\n\r\n\t/// <summary>\r\n\t/// Cleans forum code by making sure tags are properly closed, escapes HTML, removes images if settings require it, removes extra line breaks \r\n\t/// and marks up URL's and e-mail addresses as links. This method should generally not be called directly except for testing.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text with forum code to clean.</param>\r\n\t/// <returns>Cleaned forum code text.</returns>\r\n\tstring CleanForumCode(string text);\r\n\r\n\t/// <summary>\r\n\t/// Converts forum code to HTML for storage. Important: This method does NOT attempt to create valid HTML, as it assumes that the forum code is \r\n\t/// already well-formed. This method should generally not be called directly except for testing.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tstring CleanForumCodeToHtml(string text);\r\n\r\n\tstring EscapeHtmlAndCensor(string text);\r\n\tstring HtmlToForumCode(string text);\r\n\t/// <summary>\r\n\t/// Removes all forum code markup from the text. Useful for scrubbing text to be saved in search repositories.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tstring RemoveForumCode(string text);\r\n}\r\n\r\npublic class TextParsingService : ITextParsingService\r\n{\r\n\tpublic TextParsingService(ISettingsManager settingsManager)\r\n\t{\r\n\t\t_settingsManager = settingsManager;\r\n\t}\r\n\r\n\tprivate readonly ISettingsManager _settingsManager;\r\n\r\n\tpublic static string[] AllowedCloseableTags = { \"b\", \"i\", \"code\", \"pre\", \"ul\", \"ol\", \"li\", \"url\", \"quote\", \"img\" };\r\n\tprivate static readonly Regex TagPattern = new Regex(@\"\\[[\\w\"\"\\?=&/;\\+%\\*\\:~,\\!\\.\\-\\$\\|@#\\(\\)]+\\]\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n\tprivate static readonly Regex TagID = new Regex(@\"\\[/?(\\w+)\\=*.*?\\]\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n\tprivate static readonly Regex ProtocolPattern = new Regex(@\"(?<![\\]\"\"\\>=/\\w])(((news|(ht|f)tp(s?))\\://)[\\w\\-\\*]+(\\.[\\w\\-/~\\*]+)*/?)([\\w\\?=&/;\\+%\\*\\:~,\\.\\-\\$\\|@#\\(\\)])*\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n\tprivate static readonly Regex WwwPattern = new Regex(@\"(?<!(\\]|\"\"|//))(?<=\\s|^)(w{3}(\\.[\\w\\-/~\\*]+)*/?)([\\?\\w=&;\\+%\\*\\:~,\\-\\$\\|@#\\(\\)])*\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n\tprivate static readonly Regex EmailPattern = new Regex(@\"(?<=\\s|\\])(?<!(mailto:|\"\"\\]))([\\w\\.\\-_']+)@(([\\w\\-]+\\.)+[\\w\\-]+)\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n\tprivate static readonly Regex YouTubePattern = new Regex(@\"(?<![\\]\"\"\\>=])(((http(s?))\\://)[w*\\.]*(youtu\\.be|youtube\\.com+))(?!.*/shorts/)(?!.*/post/)(?!/@)([\\w\\?=&/;\\+%\\*\\:~,\\.\\-\\$\\|@#\\(\\)])*\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n\r\n\t/// <summary>\r\n\t/// Converts forum code from the browser to HTML for storage. This method wraps <see cref=\"CleanForumCode(string)\"/> and <see cref=\"ForumCodeToHtml(string)\"/>, and censors the text.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tpublic string ForumCodeToHtml(string text)\r\n\t{\r\n\t\ttext = Censor(text);\r\n\t\ttext = CleanForumCode(text);\r\n\t\ttext = CleanForumCodeToHtml(text);\r\n\t\tif (text == \"<p></p>\")\r\n\t\t\ttext = String.Empty;\r\n\t\treturn text;\r\n\t}\r\n\r\n\tpublic string EscapeHtmlAndCensor(string text)\r\n\t{\r\n\t\ttext = Censor(text);\r\n\t\treturn EscapeHtmlTags(text);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Converts client HTML from the browser to HTML for storage. This method wraps <see cref=\"ClientHtmlToForumCode(string)\"/> and <see cref=\"ForumCodeToHtml(string)\"/>, and censors the text.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tpublic string ClientHtmlToHtml(string text)\r\n\t{\r\n\t\ttext = Censor(text);\r\n\t\ttext = ClientHtmlToForumCode(text);\r\n\t\ttext = CleanForumCode(text);\r\n\t\treturn CleanForumCodeToHtml(text);\r\n\t}\r\n\r\n\tpublic string HtmlToClientHtml(string text)\r\n\t{\r\n\t\ttext = Regex.Replace(text, @\" *target=\"\"[_\\w]*\"\"\", String.Empty, RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(<iframe )(.)*?(src=\"\"https?://www.youtube.com/embed/)(\\S+)(\"\")(.)*?( */iframe>)\", \"https://www.youtube.com/watch?v=$4\", RegexOptions.IgnoreCase);\r\n\t\treturn text;\r\n\t}\r\n\r\n\tpublic string HtmlToForumCode(string text)\r\n\t{\r\n\t\ttext = HtmlToClientHtml(text);\r\n\t\ttext = ClientHtmlToForumCode(text);\r\n\t\treturn text;\r\n\t}\r\n\r\n\tpublic string Censor(string text)\r\n\t{\r\n\t\tif (String.IsNullOrEmpty(text))\r\n\t\t\treturn String.Empty;\r\n\t\t// build the censored words list\r\n\t\tvar words = _settingsManager.Current.CensorWords.Trim();\r\n\t\tif (String.IsNullOrWhiteSpace(words))\r\n\t\t\treturn text;\r\n\t\tvar cleanedCensorList = words.Replace(\"  \", \" \").Replace(\"\\r\", \" \");\r\n\t\tvar list = cleanedCensorList.Split(new[] { ' ' });\r\n\t\t// convert any stand alone words (with * before of after them) to spaces\r\n\t\tfor (var i = 0; i < list.Length; i++) { list[i] = list[i].Replace(\"*\", \" \"); }\r\n\t\t// now you've got your list of naughty words, clean them out of the text\r\n\t\tvar newWord = string.Empty;\r\n\t\tforeach (var badWord in list)\r\n\t\t{\r\n\t\t\tfor (var j = 1; j <= badWord.Length; j++) newWord += _settingsManager.Current.CensorCharacter;\r\n\t\t\ttext = Regex.Replace(text, badWord, newWord, RegexOptions.IgnoreCase);\r\n\t\t\tnewWord = String.Empty;\r\n\t\t}\r\n\t\treturn text;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Converts client HTML to forum code. Important: This method does NOT attempt to create valid HTML, as it assumes that the forum code \r\n\t/// will be cleaned. This method should generally not be called directly except for testing.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tpublic string ClientHtmlToForumCode(string text)\r\n\t{\r\n\t\ttext = text.Trim();\r\n\r\n\t\t// replace line breaks, get block elements right\r\n\t\ttext = text.Replace(\"\\r\\n\", String.Empty);\r\n\t\ttext = text.Replace(\"\\n\", String.Empty);\r\n\t\ttext = Regex.Replace(text, @\"((?<!(\\A|<blockquote>|</blockquote>|</p>))(<blockquote>|<p>))\", \"</p>$1\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(</blockquote>|</p>)((?!(<p>|<blockquote>|</blockquote>))(.*</p>))\", \"$1<p>$2\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"^<p>\", String.Empty, RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"</p>$\", String.Empty, RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"<blockquote>\", \"\\r\\n[quote]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"</blockquote>\", \"[/quote]\\r\\n\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"<p>\", \"\\r\\n\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"</p>\", \"\\r\\n\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"<br ?/?>\", \"\\r\\n\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"\\[quote\\](?!(\\r\\n))\", \"[quote]\\r\\n\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(?<!(\\r\\n))\\[/quote\\]\", \"\\r\\n[/quote]\", RegexOptions.IgnoreCase);\r\n\r\n\t\t// replace basic tags\r\n\t\ttext = Regex.Replace(text, @\"<em>\", \"[i]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</em>\", \"[/i]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<i>\", \"[i]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</i>\", \"[/i]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<strong>\", \"[b]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</strong>\", \"[/b]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<b>\", \"[b]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</b>\", \"[/b]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<code>\", \"[code]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</code>\", \"[/code]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<pre>\", \"[pre]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</pre>\", \"[/pre]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<li>\", \"[li]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</li>\", \"[/li]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<ol>\", \"[ol]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</ol>\", \"[/ol]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<ul>\", \"[ul]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</ul>\", \"[/ul]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</a>\", \"[/url]\", RegexOptions.IgnoreCase);\r\n\r\n\t\t// replace img and a tags\r\n\t\ttext = Regex.Replace(text, @\"(<a href=\"\")(\\S+)\"\"( *target=\"\"?[_\\w]*\"\"?)*>\", \"[url=$2]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<img .*?src=\"\"(\\S+)\"\".*?/?>\", \"[image=$1]\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(<iframe )(\\S+ )*(src=\"\"https?://www.youtube.com/embed/)(\\S+)(\"\")( *\\S+)*( */iframe>)\", \"[youtube=https://www.youtube.com/watch?v=$4]\", RegexOptions.IgnoreCase);\r\n\r\n\t\t// catch remaining HTML as invalid\r\n\t\ttext = Regex.Replace(text, @\"<.*?>\", String.Empty, RegexOptions.IgnoreCase);\r\n\r\n\t\t// convert HTML escapes\r\n\t\ttext = Regex.Replace(text, \"&nbsp;\", \" \", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"&amp;\", \"&\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"&lt;\", \"<\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, \"&gt;\", \">\", RegexOptions.IgnoreCase);\r\n\r\n\t\treturn text.Trim();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Cleans forum code by making sure tags are properly closed, escapes HTML, removes images if settings require it, removes extra line breaks \r\n\t/// and marks up URL's and e-mail addresses as links. This method should generally not be called directly except for testing.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text with forum code to clean.</param>\r\n\t/// <returns>Cleaned forum code text.</returns>\r\n\tpublic string CleanForumCode(string text)\r\n\t{\r\n\t\t// ditch white space\r\n\t\ttext = text.Trim();\r\n\r\n\t\t// replace lonely \\n (some browsers)\r\n\t\ttext = Regex.Replace(text, @\"((?<!\\r)\\n)\", \"\\r\\n\", RegexOptions.Multiline | RegexOptions.IgnoreCase);\r\n\r\n\t\t// remove duplicate line breaks\r\n\t\ttext = Regex.Replace(text, @\"(\\r\\n){3,}\", \"\\r\\n\\r\\n\", RegexOptions.Multiline);\r\n\r\n\t\t// handle images\r\n\t\tif (_settingsManager.Current.AllowImages)\r\n\t\t\ttext = Regex.Replace(text, @\"(\\[image){1}(?!\\=).+?\\]\", String.Empty, RegexOptions.IgnoreCase);\r\n\t\telse\r\n\t\t\ttext = Regex.Replace(text, @\"\\[image=.+?\\]\", String.Empty, RegexOptions.IgnoreCase);\r\n\r\n\t\t// close all tags\r\n\t\tvar stack = new Stack<string>();\r\n\t\tvar allMatches = TagPattern.Match(text);\r\n\t\tvar indexAdjustment = 0;\r\n\t\twhile (allMatches.Success)\r\n\t\t{\r\n\t\t\tvar tag = allMatches.ToString();\r\n\t\t\tif (!tag.StartsWith(\"[/\"))\r\n\t\t\t{\r\n\t\t\t\t// opening tag\r\n\t\t\t\tvar tagID = TagID.Replace(tag, \"$1\");\r\n\t\t\t\tif (AllowedCloseableTags.Contains(tagID))\r\n\t\t\t\t\tstack.Push(tagID);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\t// closing tag\r\n\t\t\t\tvar tagID = TagID.Replace(tag, \"$1\");\r\n\t\t\t\tif (stack.Count == 0 || !stack.Contains(tagID))\r\n\t\t\t\t{\r\n\t\t\t\t\t// prepend with opener\r\n\t\t\t\t\tif (tagID == \"url\")\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar tagIndex = allMatches.Index;\r\n\t\t\t\t\t\ttext = text.Remove(tagIndex + indexAdjustment, 6);\r\n\t\t\t\t\t\tindexAdjustment -= 6;\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse if (tagID == \"youtube\")\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar tagIndex = allMatches.Index;\r\n\t\t\t\t\t\ttext = text.Remove(tagIndex + indexAdjustment, 10);\r\n\t\t\t\t\t\tindexAdjustment -= 10;\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar opener = String.Format(\"[{0}]\", tagID);\r\n\t\t\t\t\t\ttext = opener + text;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\telse if (AllowedCloseableTags.Contains(tagID) && tagID == stack.Peek())\r\n\t\t\t\t\tstack.Pop();\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\t// close then reopen tag\r\n\t\t\t\t\tvar miniStack = new Stack<string>();\r\n\t\t\t\t\tvar tagIndex = allMatches.Index;\r\n\t\t\t\t\twhile (tagID != stack.Peek())\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tminiStack.Push(stack.Peek());\r\n\t\t\t\t\t\tvar closer = $\"[/{miniStack.Peek()}]\";\r\n\t\t\t\t\t\ttext = text.Insert(tagIndex + indexAdjustment, closer);\r\n\t\t\t\t\t\tindexAdjustment += closer.Length;\r\n\t\t\t\t\t\tstack.Pop();\r\n\t\t\t\t\t}\r\n\t\t\t\t\tstack.Pop();\r\n\t\t\t\t\twhile (miniStack.Count > 0)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar opener = $\"[{miniStack.Peek()}]\";\r\n\t\t\t\t\t\ttext = text.Insert(tagIndex + indexAdjustment + tag.Length, opener);\r\n\t\t\t\t\t\tstack.Push(miniStack.Pop());\r\n\t\t\t\t\t\tindexAdjustment += opener.Length;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tallMatches = allMatches.NextMatch();\r\n\t\t}\r\n\t\twhile (stack.Count != 0)\r\n\t\t{\r\n\t\t\t// add closers\r\n\t\t\tvar closer = $\"[/{stack.Peek()}]\";\r\n\t\t\ttext += closer;\r\n\t\t\tstack.Pop();\r\n\t\t}\r\n\r\n\t\t// put URL's in url tags (plus youtube)\r\n\t\tif (_settingsManager.Current.AllowImages)\r\n\t\t\ttext = YouTubePattern.Replace(text, match => $\"[youtube={match.Value}]\");\r\n\t\ttext = ProtocolPattern.Replace(text, match => $\"[url={match.Value}]{match.Value.Trimmer(40)}[/url]\");\r\n\t\ttext = WwwPattern.Replace(text, match => $\"[url=https://{match.Value}]{match.Value.Trimmer(40)}[/url]\");\r\n\t\ttext = EmailPattern.Replace(text, match => $\"[url=mailto:{match.Value}]{match.Value}[/url]\");\r\n\r\n\t\t// escape out rogue HTML tags\r\n\t\ttext = EscapeHtmlTags(text);\r\n\r\n\t\treturn text;\r\n\t}\r\n\r\n\tprivate static string EscapeHtmlTags(string text)\r\n\t{\r\n\t\ttext = text.Replace(\"<\", \"&lt;\");\r\n\t\ttext = text.Replace(\">\", \"&gt;\");\r\n\t\treturn text;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Converts forum code to HTML for storage. Important: This method does NOT attempt to create valid HTML, as it assumes that the forum code is \r\n\t/// already well-formed. This method should generally not be called directly except for testing.\r\n\t/// </summary>\r\n\t/// <param name=\"text\">Text to parse.</param>\r\n\t/// <returns>Parsed text.</returns>\r\n\tpublic string CleanForumCodeToHtml(string text)\r\n\t{\r\n\t\ttext = text.Trim();\r\n\r\n\t\t// replace URL tags\r\n\t\ttext = Regex.Replace(text, @\"(\\[url=\"\"?)(\\S+?)(\"\"?\\])\", \"<a href=\\\"$2\\\" target=\\\"_blank\\\">\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(<a href=\\\"\"mailto:)(\\S+?)(\\\"\" target=\\\"\"_blank\\\"\">)\", \"<a href=\\\"mailto:$2\\\">\", RegexOptions.IgnoreCase);\r\n\t\ttext = text.Replace(\"[/url]\", \"</a>\");\r\n\t\ttext = Regex.Replace(text, @\"<(?=a)\\b[^>]*>\", match => match.Value.Replace(\"javascript:\", String.Empty, StringComparison.OrdinalIgnoreCase), RegexOptions.IgnoreCase);\r\n\r\n\t\t// replace image tags\r\n\t\tif (_settingsManager.Current.AllowImages)\r\n\t\t{\r\n\t\t\ttext = Regex.Replace(text, @\"(\\[img\\])(\\S+?)(\\[/img\\])\", \"<img src=\\\"$2\\\" />\", RegexOptions.IgnoreCase);\r\n\t\t\ttext = Regex.Replace(text, @\"(\\[image=\"\"?)(\\S+?)(\"\"?\\])\", \"<img src=\\\"$2\\\" />\", RegexOptions.IgnoreCase);\r\n\t\t\ttext = ParseYouTubeTags(text);\r\n\t\t}\r\n\t\telse\r\n\t\t\ttext = Regex.Replace(text, @\"(\\[image=\"\"?)(\\S+?)(\"\"?\\])\", String.Empty, RegexOptions.IgnoreCase);\r\n\r\n\t\t// simple tags\r\n\t\ttext = text.Replace(\"[i]\", \"<em>\");\r\n\t\ttext = text.Replace(\"[/i]\", \"</em>\");\r\n\t\ttext = text.Replace(\"[b]\", \"<strong>\");\r\n\t\ttext = text.Replace(\"[/b]\", \"</strong>\");\r\n\t\ttext = text.Replace(\"[code]\", \"<code>\");\r\n\t\ttext = text.Replace(\"[/code]\", \"</code>\");\r\n\t\ttext = text.Replace(\"[pre]\", \"<pre>\");\r\n\t\ttext = text.Replace(\"[/pre]\", \"</pre>\");\r\n\t\ttext = text.Replace(\"[li]\", \"<li>\");\r\n\t\ttext = text.Replace(\"[/li]\", \"</li>\");\r\n\t\ttext = text.Replace(\"[ol]\", \"<ol>\");\r\n\t\ttext = text.Replace(\"[/ol]\", \"</ol>\");\r\n\t\ttext = text.Replace(\"[ul]\", \"<ul>\");\r\n\t\ttext = text.Replace(\"[/ul]\", \"</ul>\");\r\n\r\n\t\t// line breaks and block elements\r\n\t\ttext = Regex.Replace(text, @\"(\\r\\n){3,}\", \"\\r\\n\\r\\n\");\r\n\t\tif (!text.StartsWith(\"[quote]\") && !string.IsNullOrWhiteSpace(text)) text = \"<p>\" + text;\r\n\t\tif (!text.EndsWith(\"[/quote]\") && !string.IsNullOrWhiteSpace(text)) text += \"</p>\";\r\n\t\ttext = text.Replace(\"[quote]\", \"<blockquote>\");\r\n\t\ttext = text.Replace(\"[/quote]\", \"</blockquote>\");\r\n\t\ttext = Regex.Replace(text, @\"(?<!(</blockquote>))\\r\\n\\r\\n(?!(<p>|<blockquote>|</blockquote>))\", \"</p><p>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(?<=(</p>|<blockquote>|</blockquote>|\\A))(\\r\\n)*<blockquote>\", \"<blockquote>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(\\r\\n)+<blockquote>\", \"</p><blockquote>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<blockquote>\\r\\n(?!(<p>|<blockquote>|</blockquote>))\", \"<blockquote><p>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(?<!([</p>|<blockquote>|</blockquote>](\\r\\n)*))</blockquote>\", \"</p></blockquote>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</blockquote>(\\r\\n){2,}(?!(</p>|<blockquote>|</blockquote>))\", \"</blockquote><p>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</blockquote>(\\r\\n)*</blockquote>\", \"</blockquote></blockquote>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(?<!(</p>|<blockquote>|</blockquote>|\\A))<blockquote>\", \"</p><blockquote>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"<blockquote>(?!(<p>|<blockquote>|</blockquote>))\", \"<blockquote><p>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"(?<!(</p>|<blockquote>|</blockquote>))(\\r\\n)*</blockquote>\", \"</p></blockquote>\", RegexOptions.IgnoreCase);\r\n\t\ttext = Regex.Replace(text, @\"</blockquote>(\\r\\n)*(?!(<p>|<blockquote>|</blockquote>|\\Z))\", \"</blockquote><p>\", RegexOptions.IgnoreCase);\r\n\t\ttext = text.Replace(\"\\r\\n\", \"<br />\");\r\n\r\n\t\treturn text;\r\n\t}\r\n\r\n\tpublic string RemoveForumCode(string text)\r\n\t{\r\n\t\ttext = TagPattern.Replace(text, \"\");\r\n\t\treturn text;\r\n\t}\r\n\r\n\tprivate string ParseYouTubeTags(string text)\r\n\t{\r\n\t\tvar width = _settingsManager.Current.YouTubeWidth;\r\n\t\tvar height = _settingsManager.Current.YouTubeHeight;\r\n\t\tvar youTubeTag = new Regex(@\"(\\[youtube=\"\"?)(\\S+?)(\"\"?\\])\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n\t\tvar matches = youTubeTag.Matches(text);\r\n\t\tforeach (Match item in matches)\r\n\t\t{\r\n\t\t\tvar url = item.Groups[2].Value;\r\n\t\t\tvar uri = new Uri(url);\r\n\t\t\tif (uri.Host.Contains(\"youtube\"))\r\n\t\t\t{\r\n\t\t\t\tif (string.IsNullOrEmpty(uri.Query))\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\tvar q = uri.Query.Remove(0, 1).Split('&').Where(x => x.Contains(\"=\")).Select(x => new KeyValuePair<string, string>(x.Split('=')[0], x.Split('=')[1]));\r\n\t\t\t\tvar dictionary = q.ToDictionary(pair => pair.Key, pair => pair.Value);\r\n\t\t\t\tif (dictionary.Any(x => x.Key == \"v\"))\r\n\t\t\t\t{\r\n\t\t\t\t\ttext = text.Replace(item.Value, String.Format(@\"<iframe width=\"\"{1}\"\" height=\"\"{2}\"\" src=\"\"https://www.youtube.com/embed/{0}\"\" frameborder=\"\"0\"\" allowfullscreen></iframe>\", dictionary[\"v\"], width, height));\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\telse if (uri.Host.Contains(\"youtu.be\"))\r\n\t\t\t{\r\n\t\t\t\tvar v = uri.Segments[1];\r\n\t\t\t\ttext = text.Replace(item.Value, String.Format(@\"<iframe width=\"\"{1}\"\" height=\"\"{2}\"\" src=\"\"https://www.youtube.com/embed/{0}\"\" frameborder=\"\"0\"\" allowfullscreen></iframe>\", v, width, height));\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn text;\r\n\t}\r\n}"
  },
  {
    "path": "src/PopForums/Services/TimeFormatStringService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ITimeFormatStringService\n{\n\tTimeFormats GeTimeFormats();\n\tstring GetTimeFormatsAsJson();\n}\n\npublic class TimeFormatStringService : ITimeFormatStringService\n{\n\tpublic TimeFormats GeTimeFormats()\n\t{\n\t\tvar formats = new TimeFormats\n\t\t{\n\t\t\tTodayTime = Resources.TodayTime,\n\t\t\tYesterdayTime = Resources.YesterdayTime,\n\t\t\tMinutesAgo = Resources.MinutesAgo,\n\t\t\tOneMinuteAgo = Resources.OneMinuteAgo,\n\t\t\tLessThanMinute = Resources.LessThanMinute\n\t\t};\n\t\treturn formats;\n\t}\n\n\tpublic string GetTimeFormatsAsJson()\n\t{\n\t\tvar formats = GeTimeFormats();\n\t\tvar serialized = JsonSerializer.Serialize(formats, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n\t\treturn serialized;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/TopicService.cs",
    "content": "namespace PopForums.Services;\n\npublic interface ITopicService\n{\n\tTask<Tuple<List<Topic>, PagerContext>> GetTopics(Forum forum, bool includeDeleted, int pageIndex);\n\tTask<Topic> Get(string urlName);\n\tTask<Topic> Get(int topicID);\n\tTask CloseTopic(Topic topic, User user);\n\tTask OpenTopic(Topic topic, User user);\n\tTask PinTopic(Topic topic, User user);\n\tTask UnpinTopic(Topic topic, User user);\n\tTask DeleteTopic(Topic topic, User user);\n\tTask UndeleteTopic(Topic topic, User user);\n\tTask UpdateTitleAndForum(Topic topic, Forum forum, string newTitle, User user);\n\tTask<Tuple<List<Topic>, PagerContext>> GetTopics(User viewingUser, User postUser, bool includeDeleted, int pageIndex);\n\tTask RecalculateReplyCount(Topic topic);\n\tTask<List<Topic>> GetTopics(User viewingUser, Forum forum, bool includeDeleted);\n\tTask UpdateLast(Topic topic);\n\tTask<int> TopicLastPostID(int topicID);\n\tTask HardDeleteTopic(Topic topic, User user);\n\tTask SetAnswer(User user, Topic topic, Post post, string userUrl, string topicUrl);\n\tTask QueueTopicForIndexing(int topicID);\n\tTask CloseAgedTopics();\n}\n\npublic class TopicService : ITopicService\n{\n\tpublic TopicService(ITopicRepository topicRepository, IPostRepository postRepository, ISettingsManager settingsManager, IModerationLogService moderationLogService, IForumService forumService, IEventPublisher eventPublisher, ISearchRepository searchRepository, IUserRepository userRepository, ISearchIndexQueueRepository searchIndexQueueRepository, ITenantService tenantService, INotificationAdapter notificationAdapter)\n\t{\n\t\t_topicRepository = topicRepository;\n\t\t_postRepository = postRepository;\n\t\t_settingsManager = settingsManager;\n\t\t_moderationLogService = moderationLogService;\n\t\t_forumService = forumService;\n\t\t_eventPublisher = eventPublisher;\n\t\t_searchRepository = searchRepository;\n\t\t_userRepository = userRepository;\n\t\t_searchIndexQueueRepository = searchIndexQueueRepository;\n\t\t_tenantService = tenantService;\n\t\t_notificationAdapter = notificationAdapter;\n\t}\n\n\tprivate readonly ITopicRepository _topicRepository;\n\tprivate readonly IPostRepository _postRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IModerationLogService _moderationLogService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly IEventPublisher _eventPublisher;\n\tprivate readonly ISearchRepository _searchRepository;\n\tprivate readonly IUserRepository _userRepository;\n\tprivate readonly ISearchIndexQueueRepository _searchIndexQueueRepository;\n\tprivate readonly ITenantService _tenantService;\n\tprivate readonly INotificationAdapter _notificationAdapter;\n\n\tpublic async Task<Tuple<List<Topic>, PagerContext>> GetTopics(Forum forum, bool includeDeleted, int pageIndex)\n\t{\n\t\tvar pageSize = _settingsManager.Current.TopicsPerPage;\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar topics = await _topicRepository.Get(forum.ForumID, includeDeleted, startRow, pageSize);\n\t\tint topicCount;\n\t\tif (includeDeleted)\n\t\t\ttopicCount = await _topicRepository.GetTopicCount(forum.ForumID, true);\n\t\telse\n\t\t\ttopicCount = forum.TopicCount;\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(topicCount) / Convert.ToDouble(pageSize)));\n\t\tvar pagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\treturn Tuple.Create(topics, pagerContext);\n\t}\n\n\tpublic async Task<List<Topic>> GetTopics(User viewingUser, Forum forum, bool includeDeleted)\n\t{\n\t\tvar nonViewableForumIDs = await _forumService.GetNonViewableForumIDs(viewingUser);\n\t\tvar topics = await _topicRepository.Get(forum.ForumID, includeDeleted, nonViewableForumIDs);\n\t\treturn topics;\n\t}\n\n\tpublic async Task<Tuple<List<Topic>, PagerContext>> GetTopics(User viewingUser, User postUser, bool includeDeleted, int pageIndex)\n\t{\n\t\tvar nonViewableForumIDs = await _forumService.GetNonViewableForumIDs(viewingUser);\n\t\tvar pageSize = _settingsManager.Current.TopicsPerPage;\n\t\tvar startRow = ((pageIndex - 1) * pageSize) + 1;\n\t\tvar topics = await _topicRepository.GetTopicsByUser(postUser.UserID, includeDeleted, nonViewableForumIDs, startRow, pageSize);\n\t\tvar topicCount = await _topicRepository.GetTopicCountByUser(postUser.UserID, includeDeleted, nonViewableForumIDs);\n\t\tvar totalPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(topicCount) / Convert.ToDouble(pageSize)));\n\t\tvar pagerContext = new PagerContext { PageCount = totalPages, PageIndex = pageIndex, PageSize = pageSize };\n\t\treturn Tuple.Create(topics, pagerContext);\n\t}\n\n\tpublic async Task<Topic> Get(string urlName)\n\t{\n\t\treturn await _topicRepository.Get(urlName);\n\t}\n\n\tpublic async Task<Topic> Get(int topicID)\n\t{\n\t\treturn await _topicRepository.Get(topicID);\n\t}\n\n\tpublic async Task CloseTopic(Topic topic, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicClose, topic, null);\n\t\t\tawait _topicRepository.CloseTopic(topic.TopicID);\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator to close topic.\");\n\t}\n\n\tpublic async Task OpenTopic(Topic topic, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicOpen, topic, null);\n\t\t\tawait _topicRepository.OpenTopic(topic.TopicID);\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator to open topic.\");\n\t}\n\n\tpublic async Task PinTopic(Topic topic, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicPin, topic, null);\n\t\t\tawait _topicRepository.PinTopic(topic.TopicID);\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator to pin topic.\");\n\t}\n\n\tpublic async Task UnpinTopic(Topic topic, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicUnpin, topic, null);\n\t\t\tawait _topicRepository.UnpinTopic(topic.TopicID);\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator to unpin topic.\");\n\t}\n\n\tpublic async Task DeleteTopic(Topic topic, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Moderator) || user.UserID == topic.StartedByUserID)\n\t\t{\n\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicDelete, topic, null);\n\t\t\tawait _topicRepository.DeleteTopic(topic.TopicID);\n\t\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = topic.TopicID, IsForRemoval = true });\n\t\t\tawait RecalculateReplyCount(topic);\n\t\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\t\t_forumService.UpdateCounts(forum);\n\t\t\tawait _forumService.UpdateLast(forum);\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator or topic starter to delete topic.\");\n\t}\n\n\tpublic async Task HardDeleteTopic(Topic topic, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Admin))\n\t\t{\n\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicDeletePermanently, topic, null);\n\t\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = topic.TopicID, IsForRemoval = true });\n\t\t\tawait _topicRepository.HardDeleteTopic(topic.TopicID);\n\t\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\t\t_forumService.UpdateCounts(forum);\n\t\t\tawait _forumService.UpdateLast(forum);\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Admin to hard delete topic.\");\n\t}\n\n\tpublic async Task UndeleteTopic(Topic topic, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicUndelete, topic, null);\n\t\t\tawait _topicRepository.UndeleteTopic(topic.TopicID);\n\t\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = topic.TopicID, IsForRemoval = false });\n\t\t\tawait RecalculateReplyCount(topic);\n\t\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\t\t_forumService.UpdateCounts(forum);\n\t\t\tawait _forumService.UpdateLast(forum);\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator to undelete topic.\");\n\t}\n\n\tpublic async Task UpdateTitleAndForum(Topic topic, Forum forum, string newTitle, User user)\n\t{\n\t\tif (user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tvar oldTopic = await _topicRepository.Get(topic.TopicID);\n\t\t\tif (oldTopic.ForumID != forum.ForumID)\n\t\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicMoved, topic, forum, $\"Moved from {oldTopic.ForumID} to {forum.ForumID}\");\n\t\t\tif (oldTopic.Title != newTitle)\n\t\t\t\tawait _moderationLogService.LogTopic(user, ModerationType.TopicRenamed, topic, forum, $\"Renamed from \\\"{oldTopic.Title}\\\" to \\\"{newTitle}\\\"\");\n\t\t\tvar urlName = newTitle.ToUniqueUrlName(await _topicRepository.GetUrlNamesThatStartWith(newTitle.ToUrlName()));\n\t\t\ttopic.UrlName = urlName;\n\t\t\tawait _topicRepository.UpdateTitleAndForum(topic.TopicID, forum.ForumID, newTitle, urlName);\n\t\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = topic.TopicID, IsForRemoval = false });\n\t\t\t_forumService.UpdateCounts(forum);\n\t\t\tawait _forumService.UpdateLast(forum);\n\t\t\tvar oldForum = await _forumService.Get(oldTopic.ForumID);\n\t\t\t_forumService.UpdateCounts(oldForum);\n\t\t\tawait _forumService.UpdateLast(oldForum);\n\t\t}\n\t\telse\n\t\t\tthrow new InvalidOperationException(\"User must be Moderator to update topic title or move topic.\");\n\t}\n\n\tpublic async Task RecalculateReplyCount(Topic topic)\n\t{\n\t\tvar replyCount = await _postRepository.GetReplyCount(topic.TopicID, false);\n\t\tawait _topicRepository.UpdateReplyCount(topic.TopicID, replyCount);\n\t}\n\n\tpublic async Task UpdateLast(Topic topic)\n\t{\n\t\tvar post = await _postRepository.GetLastInTopic(topic.TopicID);\n\t\tawait _topicRepository.UpdateLastTimeAndUser(topic.TopicID, post.UserID, post.Name, post.PostTime);\n\t}\n\n\tpublic async Task<int> TopicLastPostID(int topicID)\n\t{\n\t\tvar post = await _postRepository.GetLastInTopic(topicID);\n\t\tif (post == null)\n\t\t\treturn 0;\n\t\treturn post.PostID;\n\t}\n\n\tpublic async Task SetAnswer(User user, Topic topic, Post post, string userUrl, string topicUrl)\n\t{\n\t\tif (user.UserID != topic.StartedByUserID)\n\t\t\tthrow new SecurityException(\"Only the user that started a topic may set its answer.\");\n\t\tif (post == null || post.TopicID != topic.TopicID)\n\t\t\tthrow new InvalidOperationException(\"You can't use a post as an answer unless it's a child of the topic.\");\n\t\tvar answerUser = await _userRepository.GetUser(post.UserID);\n\t\tif (answerUser != null // answer user is still valid\n\t\t    && !topic.AnswerPostID.HasValue && // an answer wasn't already chosen\n\t\t    topic.StartedByUserID != post.UserID) // the answer isn't coming from the question asker\n\t\t{\n\t\t\t// <a href=\"{0}\">{1}</a> chose an answer for the question: <a href=\"{2}\">{3}</a>\n\t\t\tvar message = String.Format(Resources.QuestionAnswered, userUrl, HtmlEncoder.Default.Encode(user.Name ?? string.Empty), topicUrl, HtmlEncoder.Default.Encode(topic.Title ?? string.Empty));\n\t\t\tawait _eventPublisher.ProcessEvent(message, answerUser, EventDefinitionService.StaticEventIDs.QuestionAnswered, false);\n\t\t}\n\t\tawait _topicRepository.UpdateAnswerPostID(topic.TopicID, post.PostID);\n\t\tawait _notificationAdapter.QuestionAnswer(user.Name, topic.Title, post.PostID, post.UserID);\n\t}\n\n\tpublic async Task QueueTopicForIndexing(int topicID)\n\t{\n\t\tawait _searchIndexQueueRepository.Enqueue(new SearchIndexPayload { TenantID = _tenantService.GetTenant(), TopicID = topicID, IsForRemoval = false });\n\t}\n\n\tpublic async Task CloseAgedTopics()\n\t{\n\t\tif (!_settingsManager.Current.IsClosingAgedTopics)\n\t\t\treturn;\n\t\tvar ageCutoff = DateTime.UtcNow.AddDays(-_settingsManager.Current.CloseAgedTopicsDays);\n\t\tvar list = await _topicRepository.CloseTopicsOlderThan(ageCutoff);\n\t\tforeach (var id in list)\n\t\t\tawait _moderationLogService.LogTopic(ModerationType.TopicCloseAuto, id);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/TopicViewLogService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface ITopicViewLogService\n{\n\tTask LogView(int? userID, int topicID);\n}\n\npublic class TopicViewLogService : ITopicViewLogService\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly ITopicViewLogRepository _topicViewLogRepository;\n\n\tpublic TopicViewLogService(IConfig config, ITopicViewLogRepository topicViewLogRepository)\n\t{\n\t\t_config = config;\n\t\t_topicViewLogRepository = topicViewLogRepository;\n\t}\n\n\tpublic async Task LogView(int? userID, int topicID)\n\t{\n\t\tif (!_config.LogTopicViews)\n\t\t\treturn;\n\t\tvar timeStamp = DateTime.UtcNow;\n\t\tawait _topicViewLogRepository.Log(userID, topicID, timeStamp);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/UserEmailReconciler.cs",
    "content": "namespace PopForums.Services;\n\npublic interface IUserEmailReconciler\n{\n\tTask<string> GetUniqueEmail(string email, string externalID);\n}\n\n/// <summary>\n/// Checks for existing email addresses from an external identity provider. If a match is found, it mangles the address\n/// to use an \"example.com\" address, which is not real per IETF RFC 2606.\n/// </summary>\npublic class UserEmailReconciler : IUserEmailReconciler\n{\n\tprivate readonly IUserRepository _userRepository;\n\n\tpublic UserEmailReconciler(IUserRepository userRepository)\n\t{\n\t\t_userRepository = userRepository;\n\t}\n\n\tpublic async Task<string> GetUniqueEmail(string email, string externalID)\n\t{\n\t\tvar match = await _userRepository.GetUserByEmail(email);\n\t\tif (match is null)\n\t\t\treturn email;\n\t\t\n\t\tvar uniqueEmail = $\"{email.Replace(\"@\",\"-at-\")}@{externalID}.example.com\";\n\t\treturn uniqueEmail;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/UserNameReconciler.cs",
    "content": "namespace PopForums.Services;\n\npublic interface IUserNameReconciler\n{\n\tTask<string> GetUniqueNameForUser(string name);\n}\n\n/// <summary>\n/// Used to make sure that incoming names from an external identity provider are unique. In other words, if there's\n/// a \"John Smith,\" the next one becomes \"John Smith-2.\"\n/// </summary>\npublic class UserNameReconciler : IUserNameReconciler\n{\n\tprivate readonly IUserRepository _userRepository;\n\n\tpublic UserNameReconciler(IUserRepository userRepository)\n\t{\n\t\t_userRepository = userRepository;\n\t}\n\n\tpublic async Task<string> GetUniqueNameForUser(string name)\n\t{\n\t\tvar existingMatches = await _userRepository.GetUserNamesThatStartWith(name);\n\t\tvar uniqueName = name.ToUniqueName(existingMatches.ToList());\n\t\treturn uniqueName;\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/UserService.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IUserService\n{\n\tTask SetPassword(User targetUser, string password, string ip, User user);\n\tTask<Tuple<bool, Guid?>> CheckPassword(string email, string password);\n\tTask<User> GetUser(int userID);\n\tTask<User> GetUserByName(string name);\n\tTask<User> GetUserByEmail(string email);\n\tTask<User> GetUserByAuhtorizationKey(Guid authorizationKey);\n\tTask<bool> IsNameInUse(string name);\n\tTask<bool> IsEmailInUse(string email);\n\tTask<User> CreateUserWithProfile(SignupData signupData, string ip);\n\tTask<User> CreateUser(string name, string email, string password, bool isApproved, string ip);\n\tTask DeleteUser(User targetUser, User user, string ip, bool ban);\n\tTask UpdateLastActivityDate(User user);\n\tTask ChangeEmail(User targetUser, string newEmail, User user, string ip);\n\tTask ChangeEmail(User targetUser, string newEmail, User user, string ip, bool isUserApproved);\n\tTask ChangeName(User targetUser, string newName, User user, string ip);\n\tTask UpdateIsApproved(User targetUser, bool isApproved, User user, string ip);\n\tTask UpdateAuthorizationKey(User user, Guid key);\n\tTask Logout(User user, string ip);\n\tTask<Tuple<bool, User>> Login(string email, string password, string ip);\n\tTask Login(User user, string ip);\n\tTask<List<string>> GetAllRoles();\n\tTask CreateRole(string role, User user, string ip);\n\tTask DeleteRole(string role, User user, string ip);\n\tTask<User> VerifyAuthorizationCode(Guid key, string ip);\n\tTask<List<User>> SearchByEmail(string email);\n\tTask<List<User>> SearchByName(string name);\n\tTask<List<User>> SearchByRole(string role);\n\tTask EditUser(User targetUser, UserEdit userEdit, bool removeAvatar, bool removePhoto, byte[] avatarFile, byte[] photoFile, string ip, User user);\n\tTask EditUserProfileImages(User user, bool removeAvatar, bool removePhoto, byte[] avatarFile, byte[] photoFile);\n\tTask<UserEdit> GetUserEdit(User user);\n\tbool IsPasswordValid(string password, out string errorMessage);\n\tTask<bool> IsEmailInUseByDifferentUser(User user, string email);\n\tTask<List<User>> GetUsersOnline();\n\tTask<bool> IsIPBanned(string ip);\n\tTask<bool> IsEmailBanned(string email);\n\tTask GeneratePasswordResetEmail(User user, string resetLink);\n\tTask ResetPassword(User user, string newPassword, string ip);\n\tTask<List<User>> GetUsersFromIDs(IList<int> ids);\n\tTask<int> GetTotalUsers();\n\tTask<List<User>> GetSubscribedUsers();\n\tDictionary<int, (User, int)> GetUsersByPointTotals(int top);\n\tTask<List<UserResult>> GetRecentUsers();\n\tTask UpdateTokenExpiration(User user, DateTime? tokenExpiration);\n\tTask UpdateRefreshToken(User user, string refreshToken);\n\tTask<string> GetRefreshToken(User user);\n}\n\npublic class UserService : IUserService\n{\n\tprivate readonly IUserRepository _userRepository;\n\tprivate readonly IRoleRepository _roleRepository;\n\tprivate readonly IProfileRepository _profileRepository;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IUserAvatarRepository _userAvatarRepository;\n\tprivate readonly IUserImageRepository _userImageRepository;\n\tprivate readonly ISecurityLogService _securityLogService;\n\tprivate readonly ITextParsingService _textParsingService;\n\tprivate readonly IBanRepository _banRepository;\n\tprivate readonly IForgotPasswordMailer _forgotPasswordMailer;\n\tprivate readonly IImageService _imageService;\n\tprivate readonly IConfig _config;\n\n\t// TODO: Dependencies on imageservice\n\tpublic UserService(IUserRepository userRepository, IRoleRepository roleRepository, IProfileRepository profileRepository, ISettingsManager settingsManager, IUserAvatarRepository userAvatarRepository, IUserImageRepository userImageRepository, ISecurityLogService securityLogService, ITextParsingService textParsingService, IBanRepository banRepository, IForgotPasswordMailer forgotPasswordMailer, IImageService imageService, IConfig config)\n\t{\n\t\t_userRepository = userRepository;\n\t\t_roleRepository = roleRepository;\n\t\t_profileRepository = profileRepository;\n\t\t_settingsManager = settingsManager;\n\t\t_userAvatarRepository = userAvatarRepository;\n\t\t_userImageRepository = userImageRepository;\n\t\t_securityLogService = securityLogService;\n\t\t_textParsingService = textParsingService;\n\t\t_banRepository = banRepository;\n\t\t_forgotPasswordMailer = forgotPasswordMailer;\n\t\t_imageService = imageService;\n\t\t_config = config;\n\t}\n\n\tpublic async Task SetPassword(User targetUser, string password, string ip, User user)\n\t{\n\t\tvar salt = Guid.NewGuid();\n\t\tvar hashedPassword = password.GetSHA256Hash(salt);\n\t\tawait _userRepository.SetHashedPassword(targetUser, hashedPassword, salt);\n\t\tawait _securityLogService.CreateLogEntry(user, targetUser, ip, string.Empty, SecurityLogType.PasswordChange);\n\t}\n\n\tpublic async Task<Tuple<bool, Guid?>> CheckPassword(string email, string password)\n\t{\n\t\tstring hashedPassword;\n\t\tvar (storedHash, salt) = await _userRepository.GetHashedPasswordByEmail(email);\n\t\tif (salt.HasValue)\n\t\t\thashedPassword = password.GetSHA256Hash(salt.Value);\n\t\telse\n\t\t\thashedPassword = password.GetSHA256Hash();\n\t\tif (storedHash == hashedPassword)\n\t\t\treturn Tuple.Create(true, salt);\n\t\t// legacy check\n\t\tvar oldResult = await CheckOldHashedPassword(email, password, salt, storedHash);\n\t\treturn Tuple.Create(oldResult, salt);\n\t}\n\n\t/// <summary>\n\t/// This method is used to maintain compatibility with really old and crusty instances of POP Forums\n\t/// that used MD5 to hash passwords. It upgrades those passwords if they match.\n\t/// </summary>\n\tprivate async Task<bool> CheckOldHashedPassword(string email, string password, Guid? salt, string storedHash)\n\t{\n\t\tstring hashedPassword;\n\t\tif (salt.HasValue)\n\t\t\thashedPassword = password.GetMD5Hash(salt.Value);\n\t\telse\n\t\t\thashedPassword = password.GetMD5Hash();\n\t\tif (storedHash == hashedPassword)\n\t\t{\n\t\t\t// upgrade the password hash\n\t\t\tvar user = await _userRepository.GetUserByEmail(email);\n\t\t\tawait SetPassword(user, password, string.Empty, null);\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tpublic async Task<User> GetUser(int userID)\n\t{\n\t\tvar user = await _userRepository.GetUser(userID);\n\t\tawait PopulateRoles(user);\n\t\treturn user;\n\t}\n\n\tpublic async Task<User> GetUserByName(string name)\n\t{\n\t\tif (string.IsNullOrWhiteSpace(name))\n\t\t\treturn null;\n\t\tname = name.ToLower();\n\t\tvar user = await _userRepository.GetUserByName(name);\n\t\tif (user == null)\n\t\t\treturn null;\n\t\tawait PopulateRoles(user);\n\t\treturn user;\n\t}\n\n\tpublic async Task<User> GetUserByAuhtorizationKey(Guid authorizationKey)\n\t{\n\t\tvar user = await _userRepository.GetUserByAuthorizationKey(authorizationKey);\n\t\tawait PopulateRoles(user);\n\t\treturn user;\n\t}\n\n\tpublic async Task<User> GetUserByEmail(string email)\n\t{\n\t\tif (string.IsNullOrWhiteSpace(email))\n\t\t\treturn null;\n\t\temail = email.ToLower();\n\t\tvar user = await _userRepository.GetUserByEmail(email);\n\t\tawait PopulateRoles(user);\n\t\treturn user;\n\t}\n\n\tpublic async Task<List<User>> GetUsersFromIDs(IList<int> ids)\n\t{\n\t\treturn await _userRepository.GetUsersFromIDs(ids);\n\t}\n\n\tprivate async Task PopulateRoles(User user)\n\t{\n\t\tif (user != null)\n\t\t\tuser.Roles = await _roleRepository.GetUserRoles(user.UserID);\n\t}\n\n\tpublic async Task<bool> IsNameInUse(string name)\n\t{\n\t\treturn await GetUserByName(name) != null;\n\t}\n\n\tpublic async Task<bool> IsEmailInUse(string email)\n\t{\n\t\treturn await GetUserByEmail(email) != null;\n\t}\n\n\tpublic async Task<bool> IsEmailInUseByDifferentUser(User user, string email)\n\t{\n\t\tvar otherUser = await GetUserByEmail(email);\n\t\tif (otherUser == null)\n\t\t\treturn false;\n\t\treturn otherUser.Email != user.Email;\n\t}\n\n\tpublic async Task<bool> IsIPBanned(string ip)\n\t{\n\t\treturn await _banRepository.IPIsBanned(ip);\n\t}\n\n\tpublic async Task<bool> IsEmailBanned(string email)\n\t{\n\t\treturn await _banRepository.EmailIsBanned(email);\n\t}\n\n\tpublic async Task<User> CreateUserWithProfile(SignupData signupData, string ip)\n\t{\n\t\tvar isApproved = _config.IsOAuthOnly || _settingsManager.Current.IsNewUserApproved;\n\t\tvar user = await CreateUser(signupData.Name, signupData.Email, signupData.Password, isApproved, ip);\n\t\tvar profile = new Profile\n\t\t{\n\t\t\tUserID = user.UserID,\n\t\t\tIsSubscribed = signupData.IsSubscribed,\n\t\t\tIsTos = signupData.IsTos,\n\t\t\tIsAutoFollowOnReply = signupData.IsAutoFollowOnReply\n\t\t};\n\t\tawait _profileRepository.Create(profile);\n\t\treturn user;\n\t}\n\n\tpublic async Task<User> CreateUser(string name, string email, string password, bool isApproved, string ip)\n\t{\n\t\tname = _textParsingService.Censor(name);\n\t\tif (!email.IsEmailAddress())\n\t\t\tthrow new Exception(\"E-mail address invalid.\");\n\t\tif (string.IsNullOrEmpty(name))\n\t\t\tthrow new Exception(\"Name must not be empty or null.\");\n\t\tif (await IsNameInUse(name))\n\t\t\tthrow new Exception($\"The name \\\"{name}\\\" is already in use.\");\n\t\tif (await IsEmailInUse(email))\n\t\t\tthrow new Exception($\"The e-mail \\\"{email}\\\" is already in use.\");\n\t\tif (await IsIPBanned(ip))\n\t\t\tthrow new Exception($\"The IP {ip} is banned.\");\n\t\tif (await IsEmailBanned(email))\n\t\t\tthrow new Exception($\"The e-mail {email} is banned.\");\n\t\tvar creationDate = DateTime.UtcNow;\n\t\tvar authorizationKey = Guid.NewGuid();\n\t\tvar salt = Guid.NewGuid();\n\t\tvar hashedPassword = password.GetSHA256Hash(salt);\n\t\tvar user = await _userRepository.CreateUser(name, email, creationDate, isApproved, hashedPassword, authorizationKey, salt);\n\t\tawait _securityLogService.CreateLogEntry(null, user, ip, string.Empty, SecurityLogType.UserCreated);\n\t\treturn user;\n\t}\n\n\tpublic async Task DeleteUser(User targetUser, User user, string ip, bool ban)\n\t{\n\t\tif (ban)\n\t\t\tawait _banRepository.BanEmail(targetUser.Email);\n\t\tawait _userRepository.DeleteUser(targetUser);\n\t\tawait _securityLogService.CreateLogEntry(user, targetUser, ip, $\"Name: {targetUser.Name}, E-mail: {targetUser.Email}\", SecurityLogType.UserDeleted);\n\t}\n\n\tpublic async Task UpdateLastActivityDate(User user)\n\t{\n\t\tawait _userRepository.UpdateLastActivityDate(user, DateTime.UtcNow);\n\t}\n\n\tpublic async Task ChangeEmail(User targetUser, string newEmail, User user, string ip)\n\t{\n\t\tawait ChangeEmail(targetUser, newEmail, user, ip, _settingsManager.Current.IsNewUserApproved);\n\t}\n\n\tpublic async Task ChangeEmail(User targetUser, string newEmail, User user, string ip, bool isUserApproved)\n\t{\n\t\tif (!newEmail.IsEmailAddress())\n\t\t\tthrow new Exception(\"E-mail address invalid.\");\n\t\tif (await IsEmailInUse(newEmail))\n\t\t\tthrow new Exception($\"The e-mail \\\"{newEmail}\\\" is already in use.\");\n\t\tvar oldEmail = targetUser.Email;\n\t\tawait _userRepository.ChangeEmail(targetUser, newEmail);\n\t\ttargetUser.Email = newEmail;\n\t\tawait _userRepository.UpdateIsApproved(targetUser, isUserApproved);\n\t\tawait _securityLogService.CreateLogEntry(user, targetUser, ip, $\"Old: {oldEmail}, New: {newEmail}\", SecurityLogType.EmailChange);\n\t}\n\n\tpublic async Task ChangeName(User targetUser, string newName, User user, string ip)\n\t{\n\t\tif (string.IsNullOrEmpty(newName))\n\t\t\tthrow new Exception(\"Name must not be empty or null.\");\n\t\tif (await IsNameInUse(newName))\n\t\t\tthrow new Exception($\"The name \\\"{newName}\\\" is already in use.\");\n\t\tvar oldName = targetUser.Name;\n\t\tawait _userRepository.ChangeName(targetUser, newName);\n\t\ttargetUser.Name = newName;\n\t\tawait _securityLogService.CreateLogEntry(user, targetUser, ip, $\"Old: {oldName}, New: {newName}\", SecurityLogType.NameChange);\n\t}\n\n\tpublic async Task UpdateIsApproved(User targetUser, bool isApproved, User user, string ip)\n\t{\n\t\tif (targetUser == null)\n\t\t\tthrow new ArgumentNullException(\"targetUser\");\n\t\tawait _userRepository.UpdateIsApproved(targetUser, isApproved);\n\t\tvar logType = isApproved ? SecurityLogType.IsApproved : SecurityLogType.IsNotApproved;\n\t\tawait _securityLogService.CreateLogEntry(user, targetUser, ip, String.Empty, logType);\n\t}\n\n\tpublic async Task UpdateAuthorizationKey(User user, Guid key)\n\t{\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(\"user\");\n\t\tawait _userRepository.UpdateAuthorizationKey(user, key);\n\t}\n\n\tpublic async Task Logout(User user, string ip)\n\t{\n\t\t// used only for logging; controller performs actual logout\n\t\tawait _securityLogService.CreateLogEntry(null, user, ip, String.Empty, SecurityLogType.Logout);\n\t}\n\n\tpublic async Task<Tuple<bool, User>> Login(string email, string password, string ip)\n\t{\n\t\tUser user;\n\t\tvar (result, salt) = await CheckPassword(email, password);\n\t\tif (result)\n\t\t{\n\t\t\tuser = await GetUserByEmail(email);\n\t\t\tawait _userRepository.UpdateLastLoginDate(user, DateTime.UtcNow);\n\t\t\tawait _securityLogService.CreateLogEntry(null, user, ip, String.Empty, SecurityLogType.Login);\n\t\t\tif (!salt.HasValue)\n\t\t\t\tawait SetPassword(user, password, ip, user);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tuser = null;\n\t\t\tawait _securityLogService.CreateLogEntry((User)null, null, ip, \"E-mail attempted: \" + email, SecurityLogType.FailedLogin);\n\t\t}\n\t\treturn Tuple.Create(result, user);\n\t}\n\n\tpublic async Task Login(User user, string ip)\n\t{\n\t\tawait _userRepository.UpdateLastLoginDate(user, DateTime.UtcNow);\n\t\tawait _securityLogService.CreateLogEntry(null, user, ip, String.Empty, SecurityLogType.Login);\n\t}\n\n\tpublic async Task<List<string>> GetAllRoles()\n\t{\n\t\treturn await _roleRepository.GetAllRoles();\n\t}\n\n\tpublic async Task CreateRole(string role, User user, string ip)\n\t{\n\t\tawait _roleRepository.CreateRole(role);\n\t\tawait _securityLogService.CreateLogEntry(user, null, ip, \"Role: \" + role, SecurityLogType.RoleCreated);\n\t}\n\n\tpublic async Task DeleteRole(string role, User user, string ip)\n\t{\n\t\tif (role.ToLower() == PermanentRoles.Admin.ToLower() || role.ToLower() == PermanentRoles.Moderator.ToLower())\n\t\t\tthrow new InvalidOperationException(\"Can't delete Admin or Moderator roles.\");\n\t\tawait _roleRepository.DeleteRole(role);\n\t\tawait _securityLogService.CreateLogEntry(user, null, ip, \"Role: \" + role, SecurityLogType.RoleDeleted);\n\t}\n\n\tpublic async Task<User> VerifyAuthorizationCode(Guid key, string ip)\n\t{\n\t\tvar targetUser = await _userRepository.GetUserByAuthorizationKey(key);\n\t\tif (targetUser == null)\n\t\t\treturn null;\n\t\tvar newKey = Guid.NewGuid();\n\t\tawait UpdateAuthorizationKey(targetUser, newKey);\n\t\tawait UpdateIsApproved(targetUser, true, null, ip);\n\t\ttargetUser.AuthorizationKey = newKey;\n\t\treturn targetUser;\n\t}\n\n\tpublic async Task<List<User>> SearchByEmail(string email)\n\t{\n\t\treturn await _userRepository.SearchByEmail(email);\n\t}\n\n\tpublic async Task<List<User>> SearchByName(string name)\n\t{\n\t\treturn await _userRepository.SearchByName(name);\n\t}\n\n\tpublic async Task<List<User>> SearchByRole(string role)\n\t{\n\t\treturn await _userRepository.SearchByRole(role);\n\t}\n\n\tpublic async Task<UserEdit> GetUserEdit(User user)\n\t{\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(\"user\");\n\t\tvar profile = await _profileRepository.GetProfile(user.UserID);\n\t\treturn new UserEdit(user, profile);\n\t}\n\n\t/// <summary>\n\t/// Used only from setup service and admin user edits.\n\t/// </summary>\n\t/// <param name=\"targetUser\"></param>\n\t/// <param name=\"userEdit\"></param>\n\t/// <param name=\"removeAvatar\"></param>\n\t/// <param name=\"removePhoto\"></param>\n\t/// <param name=\"avatarFile\"></param>\n\t/// <param name=\"photoFile\"></param>\n\t/// <param name=\"ip\"></param>\n\t/// <param name=\"user\"></param>\n\t/// <returns></returns>\n\tpublic async Task EditUser(User targetUser, UserEdit userEdit, bool removeAvatar, bool removePhoto, byte[] avatarFile, byte[] photoFile, string ip, User user)\n\t{\n\t\tif (!string.IsNullOrWhiteSpace(userEdit.NewEmail))\n\t\t\tawait ChangeEmail(targetUser, userEdit.NewEmail, user, ip, userEdit.IsApproved);\n\t\tif (!string.IsNullOrWhiteSpace(userEdit.NewPassword))\n\t\t\tawait SetPassword(targetUser, userEdit.NewPassword, ip, user);\n\t\tif (targetUser.IsApproved != userEdit.IsApproved)\n\t\t\tawait UpdateIsApproved(targetUser, userEdit.IsApproved, user, ip);\n\n\t\tvar profile = await _profileRepository.GetProfile(targetUser.UserID);\n\t\tprofile.IsSubscribed = userEdit.IsSubscribed;\n\t\tprofile.ShowDetails = userEdit.ShowDetails;\n\t\tprofile.IsPlainText = userEdit.IsPlainText;\n\t\tprofile.HideVanity = userEdit.HideVanity;\n\t\tprofile.Signature = _textParsingService.ForumCodeToHtml(userEdit.Signature);\n\t\tprofile.Location = userEdit.Location;\n\t\tprofile.Dob = userEdit.Dob;\n\t\tprofile.Web = userEdit.Web;\n\t\tprofile.Instagram = userEdit.Instagram;\n\t\tprofile.Facebook = userEdit.Facebook;\n\t\tprofile.IsAutoFollowOnReply = userEdit.IsAutoFollowOnReply;\n\t\tif (removeAvatar)\n\t\t\tprofile.AvatarID = null;\n\t\tif (removePhoto)\n\t\t\tprofile.ImageID = null;\n\t\tawait _profileRepository.Update(profile);\n\n\t\tif (!_config.IsOAuthOnly)\n\t\t{\n\t\t\tvar newRoles = userEdit.Roles ?? new string[0];\n\t\t\tawait _roleRepository.ReplaceUserRoles(targetUser.UserID, newRoles);\n\t\t\tforeach (var role in targetUser.Roles)\n\t\t\t\tif (!newRoles.Contains(role))\n\t\t\t\t\tawait _securityLogService.CreateLogEntry(user, targetUser, ip, role, SecurityLogType.UserRemovedFromRole);\n\t\t\tforeach (var role in newRoles)\n\t\t\t\tif (!targetUser.Roles.Contains(role))\n\t\t\t\t\tawait _securityLogService.CreateLogEntry(user, targetUser, ip, role, SecurityLogType.UserAddedToRole);\n\t\t}\n\t\t\n\t\tif (avatarFile != null && avatarFile.Length > 0)\n\t\t{\n\t\t\tvar avatarID = await _userAvatarRepository.SaveNewAvatar(targetUser.UserID, avatarFile, DateTime.UtcNow);\n\t\t\tprofile.AvatarID = avatarID;\n\t\t\tawait _profileRepository.Update(profile);\n\t\t}\n\n\t\tif (photoFile != null && photoFile.Length > 0)\n\t\t{\n\t\t\tvar imageID = await _userImageRepository.SaveNewImage(targetUser.UserID, 0, true, photoFile, DateTime.UtcNow);\n\t\t\tprofile.ImageID = imageID;\n\t\t\tawait _profileRepository.Update(profile);\n\t\t}\n\t}\n\n\tpublic async Task EditUserProfileImages(User user, bool removeAvatar, bool removePhoto, byte[] avatarFile, byte[] photoFile)\n\t{\n\t\tvar profile = await _profileRepository.GetProfile(user.UserID);\n\t\tif (removeAvatar)\n\t\t{\n\t\t\tawait _userAvatarRepository.DeleteAvatarsByUserID(user.UserID);\n\t\t\tprofile.AvatarID = null;\n\t\t}\n\t\tif (removePhoto)\n\t\t{\n\t\t\tawait _userImageRepository.DeleteImagesByUserID(user.UserID);\n\t\t\tprofile.ImageID = null;\n\t\t}\n\t\tawait _profileRepository.Update(profile);\n\n\t\tif (avatarFile != null && avatarFile.Length > 0)\n\t\t{\n\t\t\tawait _userAvatarRepository.DeleteAvatarsByUserID(user.UserID);\n\t\t\tvar bytes = _imageService.ConstrainResize(avatarFile, _settingsManager.Current.UserAvatarMaxWidth, _settingsManager.Current.UserAvatarMaxHeight, 70, true);\n\t\t\tvar avatarID = await _userAvatarRepository.SaveNewAvatar(user.UserID, bytes, DateTime.UtcNow);\n\t\t\tprofile.AvatarID = avatarID;\n\t\t\tawait _profileRepository.Update(profile);\n\t\t}\n\n\t\tif (photoFile != null && photoFile.Length > 0)\n\t\t{\n\t\t\tawait _userImageRepository.DeleteImagesByUserID(user.UserID);\n\t\t\tvar bytes = _imageService.ConstrainResize(photoFile, _settingsManager.Current.UserImageMaxWidth, _settingsManager.Current.UserImageMaxHeight, 70, false);\n\t\t\tvar imageID = await _userImageRepository.SaveNewImage(user.UserID, 0, _settingsManager.Current.IsNewUserImageApproved, bytes, DateTime.UtcNow);\n\t\t\tprofile.ImageID = imageID;\n\t\t\tawait _profileRepository.Update(profile);\n\t\t}\n\t}\n\n\tpublic bool IsPasswordValid(string password, out string errorMessage)\n\t{\n\t\tif (String.IsNullOrEmpty(password) || password.Length < 6)\n\t\t{\n\t\t\terrorMessage = \"Password must be at least six characters\";\n\t\t\treturn false;\n\t\t}\n\t\terrorMessage = null;\n\t\treturn true;\n\t}\n\n\tpublic async Task<List<User>> GetUsersOnline()\n\t{\n\t\treturn await _userRepository.GetUsersOnline();\n\t}\n\n\tpublic async Task<int> GetTotalUsers()\n\t{\n\t\treturn await _userRepository.GetTotalUsers();\n\t}\n\n\tpublic async Task GeneratePasswordResetEmail(User user, string resetLink)\n\t{\n\t\tif (user == null)\n\t\t\tthrow new ArgumentNullException(\"user\");\n\t\tvar newAuth = Guid.NewGuid();\n\t\tawait UpdateAuthorizationKey(user, newAuth);\n\t\tuser.AuthorizationKey = newAuth;\n\t\tvar link = resetLink + \"/\" + newAuth;\n\t\tawait _forgotPasswordMailer.ComposeAndQueue(user, link);\n\t}\n\n\tpublic async Task ResetPassword(User user, string newPassword, string ip)\n\t{\n\t\tawait SetPassword(user, newPassword, ip, null);\n\t\tawait UpdateAuthorizationKey(user, Guid.NewGuid());\n\t\tawait Login(user, ip);\n\t}\n\n\tpublic async Task<List<User>> GetSubscribedUsers()\n\t{\n\t\treturn await _userRepository.GetSubscribedUsers();\n\t}\n\n\tpublic Dictionary<int, (User, int)> GetUsersByPointTotals(int top)\n\t{\n\t\treturn _userRepository.GetUsersByPointTotals(top);\n\t}\n\n\tpublic async Task<List<UserResult>> GetRecentUsers()\n\t{\n\t\tvar userResults = await _userRepository.GetRecentUsers();\n\t\tif (!string.IsNullOrEmpty(_config.IpLookupUrlFormat))\n\t\t{\n\t\t\tforeach (var item in userResults)\n\t\t\t{\n\t\t\t\tvar url = string.Format(_config.IpLookupUrlFormat, item.IP);\n\t\t\t\titem.IP = $\"<a href=\\\"{url}\\\" target=\\\"_blank\\\">{item.IP}</a>\";\n\t\t\t}\n\t\t}\n\t\treturn userResults;\n\t}\n\n\tpublic async Task UpdateTokenExpiration(User user, DateTime? tokenExpiration)\n\t{\n\t\tawait _userRepository.UpdateTokenExpiration(user, tokenExpiration);\n\t}\n\n\tpublic async Task UpdateRefreshToken(User user, string refreshToken)\n\t{\n\t\tawait _userRepository.UpdateRefreshToken(user, refreshToken);\n\t}\n\n\tpublic async Task<string> GetRefreshToken(User user)\n\t{\n\t\treturn await _userRepository.GetRefreshToken(user);\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/UserSessionService.cs",
    "content": "namespace PopForums.Services;\n\npublic interface IUserSessionService\n{\n\tTask<int> ProcessUserRequest(User user, int? sessionID, string ip, Action deleteSession, Action<int> createSession);\n\tTask CleanUpExpiredSessions();\n\tTask<int> GetTotalSessionCount();\n}\n\npublic class UserSessionService : IUserSessionService\n{\n\tpublic UserSessionService(ISettingsManager settingsManager, IUserRepository userRepository, IUserSessionRepository userSessionRepository, ISecurityLogService securityLogService)\n\t{\n\t\t_settingsManager = settingsManager;\n\t\t_userRepository = userRepository;\n\t\t_userSessionRepository = userSessionRepository;\n\t\t_securityLogService = securityLogService;\n\t}\n\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IUserRepository _userRepository;\n\tprivate readonly IUserSessionRepository _userSessionRepository;\n\tprivate readonly ISecurityLogService _securityLogService;\n\n\tpublic const string _sessionIDCookieName = \"pf_sessionID\";\n\n\tpublic async Task<int> ProcessUserRequest(User user, int? sessionID, string ip, Action deleteSession, Action<int> createSession)\n\t{\n\t\tint? userID = null;\n\t\tif (user != null)\n\t\t\tuserID = user.UserID;\n\t\tif (sessionID == null)\n\t\t{\n\t\t\tsessionID = await StartNewSession(userID, ip, createSession);\n\t\t\tif (user != null)\n\t\t\t\tawait _userRepository.UpdateLastActivityDate(user, DateTime.UtcNow);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tif (user != null)\n\t\t\t\tawait _userRepository.UpdateLastActivityDate(user, DateTime.UtcNow);\n\t\t\tvar updateSuccess = await _userSessionRepository.UpdateSession(sessionID.Value, DateTime.UtcNow);\n\t\t\tif (!updateSuccess)\n\t\t\t\tsessionID = await StartNewSession(userID, ip, createSession);\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar isAnon = await _userSessionRepository.IsSessionAnonymous(sessionID.Value);\n\t\t\t\tif (userID.HasValue && isAnon || !userID.HasValue && !isAnon)\n\t\t\t\t{\n\t\t\t\t\tdeleteSession();\n\t\t\t\t\tawait EndAndDeleteSession(new ExpiredUserSession { UserID = null, SessionID = sessionID.Value, LastTime = DateTime.UtcNow });\n\t\t\t\t\tsessionID = await StartNewSession(userID, ip, createSession);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn sessionID.Value;\n\t}\n\n\tprivate async Task<int> StartNewSession(int? userID, string ip, Action<int> createSession)\n\t{\n\t\tif (userID.HasValue)\n\t\t{\n\t\t\tvar oldUserSession = await _userSessionRepository.GetSessionIDByUserID(userID.Value);\n\t\t\tif (oldUserSession != null)\n\t\t\t\tawait EndAndDeleteSession(oldUserSession);\n\t\t}\n\t\tvar random = new Random();\n\t\tvar sessionID = random.Next(int.MinValue, int.MaxValue);\n\t\tawait _securityLogService.CreateLogEntry(null, userID, ip, sessionID.ToString(), SecurityLogType.UserSessionStart);\n\t\tawait _userSessionRepository.CreateSession(sessionID, userID, DateTime.UtcNow);\n\t\tcreateSession(sessionID);\n\t\treturn sessionID;\n\t}\n\n\tprivate async Task EndAndDeleteSession(ExpiredUserSession oldUserSession)\n\t{\n\t\tawait _securityLogService.CreateLogEntry(null, oldUserSession.UserID, string.Empty, oldUserSession.SessionID.ToString(), SecurityLogType.UserSessionEnd, oldUserSession.LastTime);\n\t\tawait _userSessionRepository.DeleteSessions(oldUserSession.UserID, oldUserSession.SessionID);\n\t}\n\n\tpublic async Task CleanUpExpiredSessions()\n\t{\n\t\tvar cutOff = DateTime.UtcNow.Subtract(new TimeSpan(0, _settingsManager.Current.SessionLength, 0));\n\t\tvar expiredSessions = await _userSessionRepository.GetAndDeleteExpiredSessions(cutOff);\n\t\tforeach (var session in expiredSessions)\n\t\t\tawait _securityLogService.CreateLogEntry(null, session.UserID, string.Empty, session.SessionID.ToString(), SecurityLogType.UserSessionEnd, session.LastTime);\n\t}\n\n\tpublic async Task<int> GetTotalSessionCount()\n\t{\n\t\treturn await _userSessionRepository.GetTotalSessionCount();\n\t}\n}"
  },
  {
    "path": "src/PopForums/Services/UserSessionWorker.cs",
    "content": "﻿namespace PopForums.Services;\n\npublic interface IUserSessionWorker\n{\n\tvoid Execute();\n}\n\npublic class UserSessionWorker(IUserSessionService sessionService, IErrorLog errorLog) : IUserSessionWorker\n{\n\tpublic async void Execute()\n\t{\n\t\ttry\n\t\t{\n\t\t\tawait sessionService.CleanUpExpiredSessions();\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\terrorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Logging/ErrorLogRepository.cs",
    "content": "﻿using PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.Repositories;\nusing PopForums.Services;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing System;\nusing Azure.Data.Tables;\n\nnamespace PopForums.AzureKit.Logging;\n\npublic class ErrorLogRepository : IErrorLogRepository\n{\n\tprivate readonly ITenantService _tenantService;\n\tprivate readonly IConfig _config;\n\tprivate const string ErrorTableName = \"pferrorlog\";\n\tprivate TableClient _tableClient;\n\n\tpublic ErrorLogRepository(ITenantService tenantService, IConfig config)\n\t{\n\t\t_tenantService = tenantService;\n\t\t_config = config;\n\t}\n\n\tpublic async Task<ErrorLogEntry> Create(DateTime timeStamp, string message, string stackTrace, string data, ErrorSeverity severity)\n\t{\n\t\tvar tableClient = await GetTableClient();\n\t\tvar errorLog = new ErrorLogEntry\n\t\t{\n\t\t\tErrorID = 0,\n\t\t\tTimeStamp = timeStamp,\n\t\t\tMessage = message,\n\t\t\tStackTrace = stackTrace,\n\t\t\tData = data,\n\t\t\tSeverity = severity\n\t\t};\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tvar rowKey = DateTime.UtcNow.ToString(\"s\") + Random.Shared.Next(100000,999999);\n\t\tvar entry = new TableEntity($\"{DateTime.UtcNow.Year}-{DateTime.UtcNow.DayOfYear.ToString(\"D3\")}\", rowKey)\n\t\t\t{\n\t\t\t\t{ \"TenantID\", tenantID },\n\t\t\t\t{ \"Message\", errorLog.Message },\n\t\t\t\t{ \"StackTrace\", errorLog.StackTrace },\n\t\t\t\t{ \"Data\", errorLog.Data },\n\t\t\t\t{ \"Severity\", errorLog.Severity.ToString() }\n\t\t\t};\n\t\tawait tableClient.AddEntityAsync(entry);\n\t\treturn errorLog;\n\t}\n\n\tprivate async Task<TableClient> GetTableClient()\n\t{\n\t\tif (_tableClient != null)\n\t\t\treturn _tableClient;\n\t\tvar tableClient = new TableClient(_config.StorageConnectionString, ErrorTableName);\n\t\tawait tableClient.CreateIfNotExistsAsync();\n\t\t_tableClient = tableClient;\n\t\treturn tableClient;\n\t}\n\n\tpublic Task<int> GetErrorCount()\n\t{\n\t\treturn Task.FromResult(1);\n\t}\n\n\tpublic Task<List<ErrorLogEntry>> GetErrors(int startRow, int pageSize)\n\t{\n\t\tvar entry = new ErrorLogEntry {Message = \"Check table storage for errors.\"};\n\t\tvar list = new List<ErrorLogEntry> {entry};\n\t\treturn Task.FromResult(list);\n\t}\n\n\tpublic Task DeleteError(int errorID)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic async Task DeleteAllErrors()\n\t{\n\t\tvar tableClient = new TableClient(_config.StorageConnectionString, ErrorTableName);\n\t\tawait tableClient.DeleteAsync();\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/PopForums.AzureKit.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n\t  <Description>PopForums AzureKit Class Library</Description>\n\t\t<VersionPrefix>22.0.0</VersionPrefix>\n\t  <Authors>Jeff Putz</Authors>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t  <AssemblyName>PopForums.AzureKit</AssemblyName>\n\t  <PackageId>PopForums.AzureKit</PackageId>\n\t  <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>\n\t  <PackageProjectUrl>https://github.com/POPWorldMedia/POPForums</PackageProjectUrl>\n\t  <RepositoryUrl>https://github.com/POPWorldMedia/POPForums</RepositoryUrl>\n\t  <Copyright>2025, POP World Media, LLC</Copyright>\n\t  <PackageLicenseExpression>MIT</PackageLicenseExpression>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\PopForums\\PopForums.csproj\" />\n    <ProjectReference Include=\"..\\PopForums.Sql\\PopForums.Sql.csproj\" />\n    <ProjectReference Include=\"..\\PopForums.ElasticKit\\PopForums.ElasticKit.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n\t<PackageReference Include=\"Azure.Data.Tables\" Version=\"12.11.0\" />\n    <PackageReference Include=\"Azure.Search.Documents\" Version=\"11.7.0\" />\n    <PackageReference Include=\"Azure.Storage.Blobs\" Version=\"12.27.0\" />\n    <PackageReference Include=\"Azure.Storage.Queues\" Version=\"12.25.0\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.SignalR.StackExchangeRedis\" Version=\"10.0.7\" />\n    <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"10.0.7\" />\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"10.0.7\" />\n    <PackageReference Include=\"StackExchange.Redis\" Version=\"2.12.14\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/PopForums.AzureKit/PostImage/PostImageRepository.cs",
    "content": "﻿using System;\nusing System.Threading.Tasks;\nusing Azure.Storage.Blobs;\nusing Azure.Storage.Blobs.Models;\nusing PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.Repositories;\nusing PopForums.Services;\n\nnamespace PopForums.AzureKit.PostImage;\n\npublic class PostImageRepository : IPostImageRepository\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly ITenantService _tenantService;\n\n\tprivate static string _containerName = \"postimage\";\n\n\tpublic PostImageRepository(IConfig config, ITenantService tenantService)\n\t{\n\t\t_config = config;\n\t\t_tenantService = tenantService;\n\t}\n\n\tpublic async Task<PostImagePersistPayload> Persist(byte[] bytes, string contentType)\n\t{\n\t\tvar container = new BlobContainerClient(_config.StorageConnectionString, _containerName);\n\t\tawait container.CreateIfNotExistsAsync(PublicAccessType.Blob);\n\t\tvar tenant = _tenantService.GetTenant();\n\t\tif (string.IsNullOrWhiteSpace(tenant))\n\t\t\ttenant = \"_\";\n\t\tvar id = Guid.NewGuid().ToString();\n\t\tvar path = $\"{tenant}/{id}\";\n\t\tvar blob = container.GetBlobClient(path);\n\t\tvar binary = new BinaryData(bytes);\n\t\tawait blob.UploadAsync(binary);\n\t\tawait blob.SetHttpHeadersAsync(new BlobHttpHeaders {ContentType = contentType, CacheControl = \"private\"});\n\t\tvar url = _config.BaseImageBlobUrl + \"/\" + _containerName + \"/\" + path;\n\t\tvar payload = new PostImagePersistPayload { Url = url, ID = id };\n\t\treturn payload;\n\t}\n\n\tpublic async Task DeletePostImageData(string id, string tenantID)\n\t{\n\t\tvar container = new BlobContainerClient(_config.StorageConnectionString, _containerName);\n\t\tawait container.CreateIfNotExistsAsync(PublicAccessType.Blob);\n\t\tvar tenant = _tenantService.GetTenant();\n\t\tif (string.IsNullOrWhiteSpace(tenant))\n\t\t\ttenant = \"_\";\n\t\tvar path = $\"{tenant}/{id}\";\n\t\tawait container.DeleteBlobAsync(path, DeleteSnapshotsOption.IncludeSnapshots);\n\t}\n\n\t// The next three methods are not used when fetching images from Azure storage. The\n\t// default SQL implementation does use these.\n\n\tpublic Task<Models.PostImage> GetWithoutData(string id)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic Task<Models.PostImage> Get(string id)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic Task<IStreamResponse> GetImageStream(string id)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Queue/AwardCalculationQueueRepository.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing Azure.Core;\nusing Azure.Storage.Queues;\nusing PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.Repositories;\n\nnamespace PopForums.AzureKit.Queue;\n\npublic class AwardCalculationQueueRepository : IAwardCalculationQueueRepository\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly IErrorLog _errorLog;\n\tpublic const string QueueName = \"pfawardcalcqueue\";\n\n\tpublic AwardCalculationQueueRepository(IConfig config, IErrorLog errorLog)\n\t{\n\t\t_config = config;\n\t\t_errorLog = errorLog;\n\t}\n\n\tpublic async Task Enqueue(AwardCalculationPayload payload)\n\t{\n\t\tvar serializedPayload = JsonSerializer.Serialize(payload);\n\t\ttry\n\t\t{\n\t\t\tvar client = await GetQueueClient();\n\t\t\tawait client.SendMessageAsync(serializedPayload);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Warning, $\"AwardCalc enqueue failed on payload: {serializedPayload}\");\n\t\t}\n\t}\n\n#pragma warning disable 1998\n\tpublic async Task<KeyValuePair<string, int>> Dequeue()\n\t{\n\t\tthrow new NotImplementedException($\"{nameof(Dequeue)} should never be called because it's automatically bound to an Azure function.\");\n\t}\n#pragma warning restore 1998\n\n\tprivate async Task<QueueClient> GetQueueClient()\n\t{\n\t\tvar options = new QueueClientOptions\n\t\t{\n\t\t\tMessageEncoding = QueueMessageEncoding.Base64,\n\t\t\tRetry = { NetworkTimeout = TimeSpan.FromSeconds(2), MaxRetries = 1, Mode = RetryMode.Fixed, Delay = TimeSpan.Zero, MaxDelay = TimeSpan.Zero }\n\t\t};\n\t\tvar client = new QueueClient(_config.QueueConnectionString, QueueName, options);\n\t\tawait client.CreateIfNotExistsAsync();\n\t\treturn client;\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Queue/EmailQueueRepository.cs",
    "content": "﻿using System.Text.Json;\nusing System.Threading.Tasks;\nusing Azure.Storage.Queues;\nusing PopForums.Configuration;\nusing PopForums.Email;\nusing PopForums.Repositories;\n\nnamespace PopForums.AzureKit.Queue;\n\npublic class EmailQueueRepository : IEmailQueueRepository\n{\n\tprivate readonly IConfig _config;\n\tpublic const string QueueName = \"pfemailqueue\";\n\n\tpublic EmailQueueRepository(IConfig config)\n\t{\n\t\t_config = config;\n\t}\n\n\tpublic async Task Enqueue(EmailQueuePayload payload)\n\t{\n\t\tvar serializedPayload = JsonSerializer.Serialize(payload);\n\t\tvar client = await GetQueueClient();\n\t\tawait client.SendMessageAsync(serializedPayload);\n\t}\n\n\tpublic Task<EmailQueuePayload> Dequeue()\n\t{\n\t\tthrow new System.NotImplementedException($\"{nameof(Dequeue)} should never be called because it's automatically bound to an Azure function.\");\n\t}\n\n\tprivate async Task<QueueClient> GetQueueClient()\n\t{\n\t\tvar client = new QueueClient(_config.QueueConnectionString, QueueName, new QueueClientOptions\n\t\t{\n\t\t\tMessageEncoding = QueueMessageEncoding.Base64\n\t\t});\n\t\tawait client.CreateIfNotExistsAsync();\n\t\treturn client;\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Queue/SearchIndexQueueRepository.cs",
    "content": "﻿using System;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing Azure.Core;\nusing Azure.Storage.Queues;\nusing PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.Repositories;\n\nnamespace PopForums.AzureKit.Queue;\n\npublic class SearchIndexQueueRepository : ISearchIndexQueueRepository\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly IErrorLog _errorLog;\n\tpublic const string QueueName = \"pfsearchindexqueue\";\n\n\tpublic SearchIndexQueueRepository(IConfig config, IErrorLog errorLog)\n\t{\n\t\t_config = config;\n\t\t_errorLog = errorLog;\n\t}\n\n\tpublic async Task Enqueue(SearchIndexPayload payload)\n\t{\n\t\tvar serializedPayload = JsonSerializer.Serialize(payload);\n\t\ttry\n\t\t{\n\t\t\tvar client = await GetQueueClient();\n\t\t\tawait client.SendMessageAsync(serializedPayload);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Warning, $\"SearchIndex enqueue failed on payload: {serializedPayload}\");\n\t\t}\n\t}\n\n#pragma warning disable 1998\n\tpublic async Task<SearchIndexPayload> Dequeue()\n\t{\n\t\tthrow new NotImplementedException($\"{nameof(Dequeue)} should never be called because it's automatically bound to an Azure function.\");\n\t}\n#pragma warning restore 1998\n\n\tprivate async Task<QueueClient> GetQueueClient()\n\t{\n\t\tvar options = new QueueClientOptions\n\t\t{\n\t\t\tMessageEncoding = QueueMessageEncoding.Base64,\n\t\t\tRetry = { NetworkTimeout = TimeSpan.FromSeconds(2), MaxRetries = 1, Mode = RetryMode.Fixed, Delay = TimeSpan.Zero, MaxDelay = TimeSpan.Zero }\n\t\t};\n\t\tvar client = new QueueClient(_config.QueueConnectionString, QueueName, options);\n\t\tawait client.CreateIfNotExistsAsync();\n\t\treturn client;\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Queue/SubscribeNotificationRepository.cs",
    "content": "﻿using System;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing Azure.Core;\nusing Azure.Storage.Queues;\nusing PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.Repositories;\n\nnamespace PopForums.AzureKit.Queue;\n\npublic class SubscribeNotificationRepository : ISubscribeNotificationRepository\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly IErrorLog _errorLog;\n\tpublic const string QueueName = \"pfsubnotifyqueue\";\n\n\tpublic SubscribeNotificationRepository(IConfig config, IErrorLog errorLog)\n\t{\n\t\t_config = config;\n\t\t_errorLog = errorLog;\n\t}\n\n\tpublic async Task Enqueue(SubscribeNotificationPayload payload)\n\t{\n\t\tvar serializedPayload = JsonSerializer.Serialize(payload);\n\t\ttry\n\t\t{\n\t\t\tvar client = await GetQueueClient();\n\t\t\tawait client.SendMessageAsync(serializedPayload);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Warning, $\"SubNotify enqueue failed on payload: {serializedPayload}\");\n\t\t}\n\t}\n\n#pragma warning disable 1998\n\tpublic Task<SubscribeNotificationPayload> Dequeue()\n\t{\n\t\tthrow new NotImplementedException($\"{nameof(Dequeue)} should never be called because it's automatically bound to an Azure function.\");\n\t}\n#pragma warning restore 1998\n\n\tprivate async Task<QueueClient> GetQueueClient()\n\t{\n\t\tvar options = new QueueClientOptions\n\t\t{\n\t\t\tMessageEncoding = QueueMessageEncoding.Base64,\n\t\t\tRetry = { NetworkTimeout = TimeSpan.FromSeconds(2), MaxRetries = 1, Mode = RetryMode.Fixed, Delay = TimeSpan.Zero, MaxDelay = TimeSpan.Zero }\n\t\t};\n\t\tvar client = new QueueClient(_config.QueueConnectionString, QueueName, options);\n\t\tawait client.CreateIfNotExistsAsync();\n\t\treturn client;\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Redis/CacheHelper.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing Microsoft.Extensions.Caching.Memory;\nusing PopForums.Configuration;\nusing PopForums.Services;\nusing StackExchange.Redis;\n\nnamespace PopForums.AzureKit.Redis;\n\npublic class CacheHelper : ICacheHelper\n{\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly ITenantService _tenantService;\n\tprivate readonly IConfig _config;\n\tprivate readonly ICacheTelemetry _cacheTelemetry;\n\tprivate readonly RedisChannel _removeChannel = RedisChannel.Literal(\"pf.cache.remove\");\n\tprivate static ConnectionMultiplexer _cacheConnection;\n\tprivate static ConnectionMultiplexer _messageConnection;\n\tprivate static IMemoryCache _cache;\n\tprivate static readonly object SyncRoot = new object();\n\n\tprivate static class CacheTelemetryNames\n\t{\n\t\tpublic const string SetMemory = \"SetMemory\";\n\t\tpublic const string SetRedis = \"SetRedis\";\n\t\tpublic const string GetMemoryHit = \"GetMemoryHit\";\n\t\tpublic const string GetMemoryMiss = \"GetMemoryMiss\";\n\t\tpublic const string GetRedisHit = \"GetRedisHit\";\n\t\tpublic const string GetRedisMiss = \"GetRedisMiss\";\n\t\tpublic const string GetRedisError = \"GetRedisError\";\n\t\tpublic const string SetRedisError = \"SetRedisError\";\n\t}\n\n\tpublic CacheHelper(IErrorLog errorLog, ITenantService tenantService, IConfig config, ICacheTelemetry cacheTelemetry)\n\t{\n\t\t_errorLog = errorLog;\n\t\t_tenantService = tenantService;\n\t\t_config = config;\n\t\t_cacheTelemetry = cacheTelemetry;\n\t\t// Redis cache\n\t\tif (_cacheConnection == null)\n\t\t{\n\t\t\tlock (SyncRoot)\n\t\t\t{\n\t\t\t\tif (_cacheConnection == null)\n\t\t\t\t\t_cacheConnection = ConnectionMultiplexer.Connect(_config.CacheConnectionString);\n\t\t\t}\n\t\t}\n\t\t// Local cache\n\t\tif (_cache == null)\n\t\t\tSetupLocalCache();\n\t\t// Redis messaging to invalidate local cache entries\n\t\tif (_messageConnection == null)\n\t\t{\n\t\t\tlock (SyncRoot)\n\t\t\t{\n\t\t\t\t_messageConnection = ConnectionMultiplexer.Connect(_config.CacheConnectionString);\n\t\t\t\tvar db = _messageConnection.GetSubscriber();\n\t\t\t\tdb.Subscribe(_removeChannel, (_, value) =>\n\t\t\t\t{\n\t\t\t\t\tif (_cache == null)\n\t\t\t\t\t\treturn;\n\t\t\t\t\tvar valueString = value.ToString();\n\t\t\t\t\t_cache.Remove(valueString);\n\t\t\t\t\tOnRemoveCacheKey?.Invoke(valueString);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\t\n\tpublic event Action<string> OnRemoveCacheKey;\n\n\tpublic string GetEffectiveCacheKey(string key)\n\t{\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\treturn $\"{tenantID}:{key}\";\n\t}\n\n\tprivate static void SetupLocalCache()\n\t{\n\t\tlock (SyncRoot)\n\t\t{\n\t\t\tvar options = new MemoryCacheOptions();\n\t\t\t_cache = new MemoryCache(options);\n\t\t}\n\t}\n\n\tpublic void SetCacheObject(string key, object value)\n\t{\n\t\tSetCacheObject(key, value, _config.CacheSeconds);\n\t}\n\n\tpublic void SetCacheObject(string key, object value, double seconds)\n\t{\n\t\tkey = GetEffectiveCacheKey(key);\n\t\tvar timeSpan = TimeSpan.FromSeconds(seconds);\n\t\tvar options = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = timeSpan };\n\t\t_cacheTelemetry.Start();\n\t\t_cache.Set(key, value, options);\n\t\t_cacheTelemetry.End(CacheTelemetryNames.SetMemory, key);\n\t\t_cacheTelemetry.Start();\n\t\ttry\n\t\t{\n\t\t\tvar db = _cacheConnection.GetDatabase();\n\t\t\tvar serialized = JsonSerializer.Serialize(value);\n\t\t\tdb.StringSet(key, serialized, timeSpan, flags: CommandFlags.FireAndForget);\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.SetRedis, key);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.SetRedisError, key);\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Information);\n\t\t}\n\t}\n\n\tpublic void SetLongTermCacheObject(string key, object value)\n\t{\n\t\tkey = GetEffectiveCacheKey(key);\n\t\tvar options = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(60) };\n\t\t_cacheTelemetry.Start();\n\t\t_cache.Set(key, value, options);\n\t\t_cacheTelemetry.End(CacheTelemetryNames.SetMemory, key);\n\t\t_cacheTelemetry.Start();\n\t\ttry\n\t\t{\n\t\t\tvar db = _cacheConnection.GetDatabase();\n\t\t\tvar serialized = JsonSerializer.Serialize(value);\n\t\t\tdb.StringSet(key, serialized, flags: CommandFlags.FireAndForget);\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.SetRedis, key);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.SetRedisError, key);\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Information);\n\t\t}\n\t}\n\n\tpublic void SetPagedListCacheObject<T>(string rootKey, int page, List<T> value)\n\t{\n\t\trootKey = GetEffectiveCacheKey(rootKey);\n\t\t_cache.TryGetValue(rootKey, out Dictionary<int, List<T>> rootPages);\n\t\tif (rootPages == null)\n\t\t\trootPages = new Dictionary<int, List<T>>();\n\t\telse if (rootPages.ContainsKey(page))\n\t\t\trootPages.Remove(page);\n\t\trootPages.Add(page, value);\n\t\tvar options = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_config.CacheSeconds) };\n\t\t_cacheTelemetry.Start();\n\t\t_cache.Set(rootKey, rootPages, options);\n\t\t_cacheTelemetry.End(CacheTelemetryNames.SetMemory, rootKey);\n\t}\n\n\n\tpublic void RemoveCacheObject(string key)\n\t{\n\t\tkey = GetEffectiveCacheKey(key);\n\t\t_cache.Remove(key);\n\t\ttry\n\t\t{\n\t\t\tvar db = _cacheConnection.GetDatabase();\n\t\t\tdb.KeyDelete(key);\n\t\t\tvar bus = _messageConnection.GetDatabase();\n\t\t\tbus.Publish(_removeChannel, key, CommandFlags.FireAndForget);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Information);\n\t\t}\n\t}\n\n\tpublic T GetCacheObject<T>(string key)\n\t{\n\t\tkey = GetEffectiveCacheKey(key);\n\t\t_cacheTelemetry.Start();\n\t\tvar cacheObject = _cache.Get(key);\n\t\tif (cacheObject != null)\n\t\t{\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.GetMemoryHit, key);\n\t\t\treturn (T)cacheObject;\n\t\t}\n\t\t_cacheTelemetry.End(CacheTelemetryNames.GetMemoryMiss, key);\n\t\ttry\n\t\t{\n\t\t\tvar db = _cacheConnection.GetDatabase();\n\t\t\t_cacheTelemetry.Start();\n\t\t\tvar result = db.StringGet(key);\n\t\t\tif (string.IsNullOrEmpty(result))\n\t\t\t{\n\t\t\t\t_cacheTelemetry.End(CacheTelemetryNames.GetRedisMiss, key);\n\t\t\t\treturn default;\n\t\t\t}\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.GetRedisHit, key);\n\t\t\tvar deserialized = JsonSerializer.Deserialize<T>(result.ToString());\n\t\t\tvar timeSpan = TimeSpan.FromSeconds(_config.CacheSeconds);\n\t\t\tvar options = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = timeSpan };\n\t\t\t_cacheTelemetry.Start();\n\t\t\t_cache.Set(key, deserialized, options);\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.SetMemory, key);\n\t\t\treturn deserialized;\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.GetRedisError, key);\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Information);\n\t\t\treturn default;\n\t\t}\n\t}\n\n\tpublic List<T> GetPagedListCacheObject<T>(string rootKey, int page)\n\t{\n\t\trootKey = GetEffectiveCacheKey(rootKey);\n\t\t_cacheTelemetry.Start();\n\t\t_cache.TryGetValue(rootKey, out Dictionary<int, List<T>> rootPages);\n\t\tif (rootPages == null)\n\t\t{\n\t\t\t_cacheTelemetry.End(CacheTelemetryNames.GetMemoryMiss, rootKey);\n\t\t\treturn null;\n\t\t}\n\t\t_cacheTelemetry.End(CacheTelemetryNames.GetMemoryHit, rootKey);\n\t\tif (rootPages.ContainsKey(page))\n\t\t\treturn rootPages[page];\n\t\treturn null;\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Redis/CacheTelemetrySink.cs",
    "content": "﻿namespace PopForums.AzureKit.Redis;\n\npublic class CacheTelemetrySink : ICacheTelemetry\n{\n\tpublic void Start()\n\t{\n\t}\n\n\tpublic void End(string eventName, string key)\n\t{\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Redis/ICacheTelemetry.cs",
    "content": "﻿namespace PopForums.AzureKit.Redis;\n\npublic interface ICacheTelemetry\n{\n\tvoid Start();\n\tvoid End(string eventName, string key);\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Search/SearchIndexSubsystem.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Azure;\nusing Azure.Search.Documents;\nusing Azure.Search.Documents.Indexes;\nusing Azure.Search.Documents.Indexes.Models;\nusing Azure.Search.Documents.Models;\nusing PopForums.Configuration;\nusing PopForums.Services;\n\nnamespace PopForums.AzureKit.Search;\n\npublic class SearchIndexSubsystem : ISearchIndexSubsystem\n{\n\tprivate readonly ITextParsingService _textParsingService;\n\tprivate readonly IPostService _postService;\n\tprivate readonly IConfig _config;\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly IErrorLog _errorLog;\n\tpublic static string IndexName = \"popforumstopics\";\n\n\tpublic SearchIndexSubsystem(ITextParsingService textParsingService, IPostService postService, IConfig config, ITopicService topicService, IErrorLog errorLog)\n\t{\n\t\t_textParsingService = textParsingService;\n\t\t_postService = postService;\n\t\t_config = config;\n\t\t_topicService = topicService;\n\t\t_errorLog = errorLog;\n\t}\n\n\tpublic void DoIndex(int topicID, string tenantID, bool isForRemoval)\n\t{\n\t\tif (isForRemoval)\n\t\t{\n\t\t\tRemoveIndex(topicID, tenantID);\n\t\t\treturn;\n\t\t}\n\n\t\tvar topic = _topicService.Get(topicID).Result;\n\t\tif (topic != null)\n\t\t{\n\t\t\tvar posts = _postService.GetPosts(topic, false).Result.ToArray();\n\t\t\tvar parsedPosts = posts.Select(x =>\n\t\t\t{\n\t\t\t\tvar parsedText = _textParsingService.ClientHtmlToForumCode(x.FullText);\n\t\t\t\tparsedText = _textParsingService.RemoveForumCode(parsedText); \n\t\t\t\treturn parsedText;\n\t\t\t}).ToArray();\n\t\t\tvar joinedPosts = string.Join(\" \", parsedPosts);\n\t\t\tvar searchTopic = new SearchTopic\n\t\t\t{\n\t\t\t\tKey = $\"{tenantID}-{topicID}\",\n\t\t\t\tTopicID = topic.TopicID.ToString(),\n\t\t\t\tForumID = topic.ForumID,\n\t\t\t\tTitle = topic.Title,\n\t\t\t\tLastPostTime = topic.LastPostTime,\n\t\t\t\tStartedByName = topic.StartedByName,\n\t\t\t\tReplies = topic.ReplyCount,\n\t\t\t\tViews = topic.ViewCount,\n\t\t\t\tIsClosed = topic.IsClosed,\n\t\t\t\tIsPinned = topic.IsPinned,\n\t\t\t\tUrlName = topic.UrlName,\n\t\t\t\tLastPostName = topic.LastPostName,\n\t\t\t\tPosts = joinedPosts,\n\t\t\t\tTenantID = tenantID\n\t\t\t};\n\n\t\t\tvar batch = IndexDocumentsBatch.Create(IndexDocumentsAction.Upload(searchTopic));\n\n\t\t\ttry\n\t\t\t{\n\t\t\t\tCreateIndex();\n\t\t\t\tvar searchClient = new SearchClient(new Uri(_config.SearchUrl), IndexName, new AzureKeyCredential(_config.SearchKey));\n\t\t\t\tsearchClient.IndexDocuments(batch);\n\t\t\t}\n\t\t\tcatch (Exception exc)\n\t\t\t{\n\t\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic void RemoveIndex(int topicID, string tenantID)\n\t{\n\t\tvar key = $\"{tenantID}-{topicID}\";\n\t\ttry\n\t\t{\n\t\t\tvar searchClient = new SearchClient(new Uri(_config.SearchUrl), IndexName, new AzureKeyCredential(_config.SearchKey));\n\t\t\tsearchClient.DeleteDocuments(\"key\", new[] {key});\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n\n\tprivate void CreateIndex()\n\t{\n\t\tvar searchIndexClient = new SearchIndexClient(new Uri(_config.SearchUrl), new AzureKeyCredential(_config.SearchKey));\n\t\tvar indexResult = searchIndexClient.GetIndexNames();\n\t\tif (indexResult.Contains(IndexName))\n\t\t\treturn;\n\t\t\t\n\t\tvar indexDefinition = new SearchIndex(IndexName)\n\t\t{\n\t\t\tFields =\n\t\t\t{\n\t\t\t\tnew SimpleField(\"Key\", SearchFieldDataType.String) {IsKey = true},\n\t\t\t\tnew SimpleField(\"TopicID\", SearchFieldDataType.String),\n\t\t\t\tnew SimpleField(\"ForumID\", SearchFieldDataType.Int32) {IsFilterable = true},\n\t\t\t\tnew SearchableField(\"Title\") {IsSortable = true},\n\t\t\t\tnew SimpleField(\"LastPostTime\", SearchFieldDataType.DateTimeOffset) {IsSortable = true},\n\t\t\t\tnew SearchableField(\"StartedByName\") {IsSortable = true},\n\t\t\t\tnew SimpleField(\"Replies\", SearchFieldDataType.Int32) {IsSortable = true},\n\t\t\t\tnew SimpleField(\"Views\", SearchFieldDataType.Int32) {IsSortable = true},\n\t\t\t\tnew SimpleField(\"IsClosed\", SearchFieldDataType.Boolean) {IsSortable = false},\n\t\t\t\tnew SimpleField(\"IsPinned\", SearchFieldDataType.Boolean) {IsSortable = false},\n\t\t\t\tnew SimpleField(\"UrlName\", SearchFieldDataType.String) {IsSortable = false},\n\t\t\t\tnew SimpleField(\"LastPostName\", SearchFieldDataType.String) {IsSortable = false},\n\t\t\t\tnew SearchableField(\"Posts\") {IsSortable = false},\n\t\t\t\tnew SearchableField(\"TenantID\")\n\t\t\t}\n\t\t};\n\t\tvar weights = new TextWeights(new Dictionary<string, double> {{\"Title\", 10}, {\"StartedByName\", 5}, {\"Posts\", 1}});\n\t\tindexDefinition.ScoringProfiles.Add(\n\t\t\tnew ScoringProfile(\"TopicWeight\") {TextWeights = weights});\n\t\tsearchIndexClient.CreateIndex(indexDefinition);\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Search/SearchRepository.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Azure.Search.Documents;\nusing PopForums.Configuration;\nusing PopForums.Sql;\nusing PopForums.Models;\nusing PopForums.Repositories;\nusing Azure;\n\n#pragma warning disable 1998\n\nnamespace PopForums.AzureKit.Search;\n\npublic class SearchRepository : Sql.Repositories.SearchRepository\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly ITopicRepository _topicRepository;\n\n\tpublic SearchRepository(ISqlObjectFactory sqlObjectFactory, IConfig config, IErrorLog errorLog, ITopicRepository topicRepository) : base(sqlObjectFactory)\n\t{\n\t\t_config = config;\n\t\t_errorLog = errorLog;\n\t\t_topicRepository = topicRepository;\n\t}\n\n\tpublic override async Task<List<string>> GetJunkWords()\n\t{\n\t\treturn new List<string>();\n\t}\n\n\tpublic override async Task CreateJunkWord(string word)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic override async Task DeleteJunkWord(string word)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic override async Task DeleteAllIndexedWordsForTopic(int topicID)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic override async Task SaveSearchWord(int topicID, string word, int rank)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic override async Task<Tuple<PopForums.Models.Response<List<Topic>>, int>> SearchTopics(string searchTerm, List<int> hiddenForums, SearchType searchType, int startRow, int pageSize)\n\t{\n\t\tint topicCount;\n\t\tvar searchClient = new SearchClient(new Uri(_config.SearchUrl), SearchIndexSubsystem.IndexName, new AzureKeyCredential(_config.SearchKey));\n\t\ttry\n\t\t{\n\t\t\tvar options = new SearchOptions();\n\t\t\tswitch (searchType)\n\t\t\t{\n\t\t\t\tcase SearchType.Date:\n\t\t\t\t\toptions.OrderBy.Add(\"LastPostTime desc\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase SearchType.Name:\n\t\t\t\t\toptions.OrderBy.Add(\"StartedByName\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase SearchType.Replies:\n\t\t\t\t\toptions.OrderBy.Add(\"Replies desc\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase SearchType.Title:\n\t\t\t\t\toptions.OrderBy.Add(\"Title\");\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (startRow > 1)\n\t\t\t\toptions.Skip = startRow - 1;\n\t\t\toptions.Size = pageSize;\n\t\t\tif (hiddenForums != null && hiddenForums.Any())\n\t\t\t{\n\t\t\t\tvar neConditions = hiddenForums.Select(x => \"ForumID ne \" + x);\n\t\t\t\toptions.Filter = string.Join(\" and \", neConditions);\n\t\t\t}\n\t\t\toptions.IncludeTotalCount = true;\n\t\t\toptions.Select.Add(\"TopicID\");\n\t\t\tvar result = searchClient.Search<SearchTopic>(searchTerm, options);\n\t\t\tvar resultModels = result.Value.GetResults();\n\t\t\tvar topicIDs = resultModels.Select(x => Convert.ToInt32(x.Document.TopicID));\n\t\t\tvar topics = await _topicRepository.Get(topicIDs);\n\t\t\ttopicCount = Convert.ToInt32(result.Value.TotalCount);\n\t\t\treturn Tuple.Create(new PopForums.Models.Response<List<Topic>>(topics), topicCount);\n\t\t}\n\t\tcatch (Exception exception)\n\t\t{\n\t\t\t_errorLog.Log(exception, ErrorSeverity.Error);\n\t\t\ttopicCount = 0;\n\t\t\treturn Tuple.Create(new PopForums.Models.Response<List<Topic>>(null, false, exception), topicCount);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit/Search/SearchTopic.cs",
    "content": "﻿using System;\n\nnamespace PopForums.AzureKit.Search;\n\npublic class SearchTopic\n{\n\tpublic string Key { get; set; }\n\tpublic string TopicID { get; set; }\n\tpublic int ForumID { get; set; }\n\tpublic string Title { get; set; }\n\tpublic DateTime LastPostTime { get; set; }\n\tpublic string StartedByName { get; set; }\n\tpublic int Replies { get; set; }\n\tpublic int Views { get; set; }\n\tpublic bool IsClosed { get; set; }\n\tpublic bool IsPinned { get; set; }\n\tpublic string UrlName { get; set; }\n\tpublic string LastPostName { get; set; }\n\tpublic string Posts { get; set; }\n\tpublic string TenantID { get; set; }\n}"
  },
  {
    "path": "src/PopForums.AzureKit/ServiceCollectionExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.SignalR;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing PopForums.AzureKit.PostImage;\nusing PopForums.AzureKit.Queue;\nusing PopForums.AzureKit.Redis;\nusing PopForums.Configuration;\nusing PopForums.Repositories;\nusing PopForums.Services;\n\nnamespace PopForums.AzureKit;\n\npublic static class ServiceCollectionExtensions\n{\n\tpublic static IServiceCollection AddPopForumsRedisCache(this IServiceCollection services)\n\t{\n\t\tservices.AddTransient<ICacheTelemetry, CacheTelemetrySink>();\n\t\tvar serviceProvider = services.BuildServiceProvider();\n\t\tvar config = serviceProvider.GetService<IConfig>();\n\t\tif (config.ForceLocalOnly)\n\t\t\treturn services;\n\t\tservices.Replace(ServiceDescriptor.Singleton<ICacheHelper, PopForums.AzureKit.Redis.CacheHelper>());\n\t\treturn services;\n\t}\n\n\tpublic static ISignalRServerBuilder AddRedisBackplaneForPopForums(this ISignalRServerBuilder signalRServerBuilder)\n\t{\n\t\tvar serviceProvider = signalRServerBuilder.Services.BuildServiceProvider();\n\t\tvar config = serviceProvider.GetService<IConfig>();\n\t\tsignalRServerBuilder.AddStackExchangeRedis(config.CacheConnectionString);\n\t\treturn signalRServerBuilder;\n\t}\n\n\tpublic static IServiceCollection AddPopForumsAzureSearch(this IServiceCollection services)\n\t{\n\t\tservices.Replace(ServiceDescriptor.Transient<ISearchRepository, PopForums.AzureKit.Search.SearchRepository>());\n\t\tservices.Replace(ServiceDescriptor.Transient<ISearchIndexSubsystem, PopForums.AzureKit.Search.SearchIndexSubsystem>());\n\t\treturn services;\n\t}\n\n\tpublic static IServiceCollection AddPopForumsAzureFunctionsAndQueues(this IServiceCollection services)\n\t{\n\t\tservices.Replace(ServiceDescriptor.Transient<IEmailQueueRepository, PopForums.AzureKit.Queue.EmailQueueRepository>());\n\t\tservices.Replace(ServiceDescriptor.Transient<IAwardCalculationQueueRepository, PopForums.AzureKit.Queue.AwardCalculationQueueRepository>());\n\t\tservices.Replace(ServiceDescriptor.Transient<ISearchIndexQueueRepository, PopForums.AzureKit.Queue.SearchIndexQueueRepository>());\n\t\tservices.Replace(ServiceDescriptor.Transient<ISubscribeNotificationRepository, PopForums.AzureKit.Queue.SubscribeNotificationRepository>());\n\t\treturn services;\n\t}\n\n\tpublic static IServiceCollection AddPopForumsAzureBlobStorageForPostImages(this IServiceCollection services)\n\t{\n\t\tservices.Replace(ServiceDescriptor.Transient<IPostImageRepository, PopForums.AzureKit.PostImage.PostImageRepository>());\n\t\treturn services;\n\t}\n\n\tpublic static IServiceCollection AddPopForumsTableStorageLogging(this IServiceCollection services)\n\t{\n\t\tservices.Replace(ServiceDescriptor.Transient<IErrorLogRepository, PopForums.AzureKit.Logging.ErrorLogRepository>());\n\t\treturn services;\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/.gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# Azure Functions localsettings file\nlocal.settings.json\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# DNX\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# TODO: Comment the next line if you want to checkin your web deploy settings\n# but database connection strings (with potential passwords) will be unencrypted\n#*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/packages/repositories.config\n# NuGet v3's project.json files produces more ignoreable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\nnode_modules/\norleans.codegen.cs\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush\n.cr/\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/AwardCalculationProcessor.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing Microsoft.Azure.Functions.Worker;\nusing Microsoft.Extensions.Logging;\nusing PopForums.AzureKit.Queue;\nusing PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.ScoringGame;\nusing PopForums.Services;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class AwardCalculationProcessor\n{\n\tprivate readonly IAwardCalculator _awardCalculator;\n\tprivate readonly IServiceHeartbeatService _serviceHeartbeatService;\n\tprivate readonly IErrorLog _errorLog;\n\n\tpublic AwardCalculationProcessor(IAwardCalculator awardCalculator, IServiceHeartbeatService serviceHeartbeatService, IErrorLog errorLog)\n\t{\n\t\t_awardCalculator = awardCalculator;\n\t\t_serviceHeartbeatService = serviceHeartbeatService;\n\t\t_errorLog = errorLog;\n\t}\n\n\t[Function(\"AwardCalculationProcessor\")]\n\tpublic async Task Run([QueueTrigger(AwardCalculationQueueRepository.QueueName)] string jsonPayload, FunctionContext executionContext)\n\t{\n\t\tvar logger = executionContext.GetLogger(\"AzureFunction\");\n\t\tvar stopwatch = new Stopwatch();\n\t\tstopwatch.Start();\n\n\t\ttry\n\t\t{\n\t\t\tvar payload = JsonSerializer.Deserialize<AwardCalculationPayload>(jsonPayload);\n\t\t\tawait _awardCalculator.ProcessCalculation(payload.EventDefinitionID, payload.UserID);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t\tlogger.LogError(exc, $\"Exception thrown running {nameof(AwardCalculationProcessor)}\");\n\t\t}\n\n\t\tstopwatch.Stop();\n\t\tlogger.LogInformation($\"C# Queue AwardCalculationProcessor function processed ({stopwatch.ElapsedMilliseconds}ms): {jsonPayload}\");\n\t\tawait _serviceHeartbeatService.RecordHeartbeat(typeof(AwardCalculationProcessor).FullName, \"AzureFunction\");\n\t}\n}\n"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/BrokerSink.cs",
    "content": "﻿using PopForums.Messaging;\nusing PopForums.Models;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class BrokerSink : IBroker\n{\n\tpublic void NotifyNewPosts(Topic topic, int lasPostID)\n\t{\n\t\tthrow new System.NotImplementedException();\n\t}\n\n\tpublic void NotifyForumUpdate(Forum forum)\n\t{\n\t\tthrow new System.NotImplementedException();\n\t}\n\n\tpublic void NotifyTopicUpdate(Topic topic, Forum forum, string topicLink)\n\t{\n\t\tthrow new System.NotImplementedException();\n\t}\n\n\tpublic void NotifyNewPost(Topic topic, int postID)\n\t{\n\t\tthrow new System.NotImplementedException();\n\t}\n\n\tpublic void NotifyPMCount(int userID, int pmCount)\n\t{\n\t\tthrow new System.NotImplementedException();\n\t}\n\n\tpublic void NotifyUser(Notification notification)\n\t{\n\t\tthrow new System.NotImplementedException();\n\t}\n\n\tpublic void NotifyUser(Notification notification, string tenantID)\n\t{\n\t\tthrow new System.NotImplementedException();\n\t}\n\n\tpublic void SendPMMessage(PrivateMessagePost post)\n\t{\n\t\tthrow new System.NotImplementedException();\n\t}\n}\n"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/CacheHelper.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing PopForums.Configuration;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class CacheHelper : ICacheHelper\n{\n\tpublic void SetCacheObject(string key, object value)\n\t{\n\t}\n\n\tpublic void SetCacheObject(string key, object value, double seconds)\n\t{\n\t}\n\n\tpublic void SetLongTermCacheObject(string key, object value)\n\t{\n\t}\n\n\tpublic void SetPagedListCacheObject<T>(string rootKey, int page, List<T> value)\n\t{\n\t}\n\n\tpublic void RemoveCacheObject(string key)\n\t{\n\t}\n\n\tpublic T GetCacheObject<T>(string key)\n\t{\n\t\treturn default;\n\t}\n\n\tpublic List<T> GetPagedListCacheObject<T>(string rootKey, int page)\n\t{\n\t\treturn null;\n\t}\n\t\n#pragma warning disable CS0067\t\n\tpublic event Action<string> OnRemoveCacheKey;\n#pragma warning restore CS0067\n\t\n\tpublic string GetEffectiveCacheKey(string key)\n\t{\n\t\treturn key;\n\t}\n}\n"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/CloseAgedTopicsProcessor.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Azure.Functions.Worker;\nusing Microsoft.Extensions.Logging;\nusing PopForums.Configuration;\nusing PopForums.Services;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class CloseAgedTopicsProcessor\n{\n    private readonly ITopicService _topicService;\n    private readonly IServiceHeartbeatService _serviceHeartbeatService;\n    private readonly IErrorLog _errorLog;\n\n    public CloseAgedTopicsProcessor(ITopicService topicService, IServiceHeartbeatService serviceHeartbeatService, IErrorLog errorLog)\n    {\n\t    _topicService = topicService;\n\t    _serviceHeartbeatService = serviceHeartbeatService;\n\t    _errorLog = errorLog;\n    }\n\n    [Function(\"CloseAgedTopicsProcessor\")]\n    public async Task Run([TimerTrigger(\"0 0 */12 * * *\")]TimerInfo myTimer, FunctionContext executionContext)\n\t{\n\t\tvar logger = executionContext.GetLogger(\"AzureFunction\");\n\t\tvar stopwatch = new Stopwatch();\n\t\tstopwatch.Start();\n\n\t\ttry\n\t\t{\n\t\t\tawait _topicService.CloseAgedTopics();\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t\tlogger.LogError(exc, $\"Exception thrown running {nameof(CloseAgedTopicsProcessor)}\");\n\t\t}\n\n\t\tstopwatch.Stop();\n\t\tlogger.LogInformation($\"C# Timer {nameof(CloseAgedTopicsProcessor)} function executed ({stopwatch.ElapsedMilliseconds}ms) at: {DateTime.UtcNow}\");\n\t\tawait _serviceHeartbeatService.RecordHeartbeat(typeof(CloseAgedTopicsProcessor).FullName, \"AzureFunction\");\n\t}\n}\n"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/EmailProcessor.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing Microsoft.Azure.Functions.Worker;\nusing Microsoft.Extensions.Logging;\nusing PopForums.Configuration;\nusing PopForums.Email;\nusing PopForums.Models;\nusing PopForums.Repositories;\nusing PopForums.Services;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class EmailProcessor\n{\n    private readonly IQueuedEmailMessageRepository _queuedEmailRepo;\n    private readonly ISmtpWrapper _smtpWrapper;\n    private readonly IServiceHeartbeatService _serviceHeartbeatService;\n    private readonly IErrorLog _errorLog;\n\n    public EmailProcessor(IQueuedEmailMessageRepository queuedEmailRepo, ISmtpWrapper smtpWrapper, IServiceHeartbeatService serviceHeartbeatService, IErrorLog errorLog)\n    {\n\t    _queuedEmailRepo = queuedEmailRepo;\n\t    _smtpWrapper = smtpWrapper;\n\t    _serviceHeartbeatService = serviceHeartbeatService;\n\t    _errorLog = errorLog;\n    }\n\n    [Function(\"EmailProcessor\")]\n    public async Task RunAsync([QueueTrigger(PopForums.AzureKit.Queue.EmailQueueRepository.QueueName)]string jsonPayload, FunctionContext executionContext)\n\t{\n\t\tvar logger = executionContext.GetLogger(\"AzureFunction\");\n\t\tvar stopwatch = new Stopwatch();\n\t\tstopwatch.Start();\n\n\t\tQueuedEmailMessage message = null;\n\t\ttry\n\t\t{\n\t\t\tvar payload = JsonSerializer.Deserialize<EmailQueuePayload>(jsonPayload);\n\t\t\tmessage = _queuedEmailRepo.GetMessage(payload.MessageID).Result;\n\t\t\tif (payload.EmailQueuePayloadType == EmailQueuePayloadType.MassMessage)\n\t\t\t{\n\t\t\t\tmessage.ToEmail = payload.ToEmail;\n\t\t\t\tmessage.ToName = payload.ToName;\n\t\t\t}\n\t\t\t_smtpWrapper.Send(message);\n\t\t\tawait _queuedEmailRepo.DeleteMessage(message.MessageID);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\tif (message == null)\n\t\t\t\t_errorLog.Log(exc, ErrorSeverity.Email, \"There was no message for the MailWorker to send.\");\n\t\t\telse\n\t\t\t\t_errorLog.Log(exc, ErrorSeverity.Email, $\"MessageID: {message.MessageID}, To: <{message.ToEmail}> {message.ToName}, Subject: {message.Subject}\");\n\t\t\tlogger.LogError(exc, $\"Exception thrown running {nameof(EmailProcessor)}\");\n\t\t}\n\t\tstopwatch.Stop();\n\t\tlogger.LogInformation($\"C# Queue {nameof(EmailProcessor)} function processed ({stopwatch.ElapsedMilliseconds}ms): {jsonPayload}\");\n\t\ttry\n\t\t{\n\t\t\tawait _serviceHeartbeatService.RecordHeartbeat(typeof(EmailProcessor).FullName, \"AzureFunction\");\n\t\t}\n\t\tcatch(Exception exc)\n\t\t{\n\t\t\t// we don't want to risk spamming anyone because of a database failure\n\t\t\tlogger.LogError(exc, $\"Logging the service heartbeat for {nameof(EmailProcessor)} failed.\");\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/NotificationTunnel.cs",
    "content": "﻿using System;\nusing System.Net.Http;\nusing System.Net.Http.Json;\nusing PopForums.Configuration;\nusing PopForums.Extensions;\nusing PopForums.Messaging;\nusing PopForums.Messaging.Models;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class NotificationTunnel : INotificationTunnel\n{\n\tprivate readonly IConfig _config;\n\n\tpublic NotificationTunnel(IConfig config)\n\t{\n\t\t_config = config;\n\t}\n\n\tpublic void SendNotificationForUserAward(string title, int userID, string tenantID)\n\t{\n\t\tvar payload = new AwardPayload\n\t\t{\n\t\t\tTitle = title,\n\t\t\tUserID = userID,\n\t\t\tTenantID = tenantID\n\t\t};\n\t\tvar url = _config.WebAppUrlAndArea + \"/api/notifyaward\";\n\t\tSendMessage(url, payload);\n\t}\n\n\tpublic void SendNotificationForReply(string postName, string title, int topicID, int userID, string tenantID)\n\t{\n\t\tvar payload = new ReplyPayload\n\t\t{\n\t\t\tPostName = postName,\n\t\t\tTitle = title,\n\t\t\tTopicID = topicID,\n\t\t\tUserID = userID,\n\t\t\tTenantID = tenantID\n\t\t};\n\t\tvar url = _config.WebAppUrlAndArea + \"/api/notifyreply\";\n\t\tSendMessage(url, payload);\n\t}\n\n\tprivate void SendMessage(string url, object payload)\n\t{\n\t\tvar authHash = _config.QueueConnectionString.GetSHA256Hash();\n\t\tusing var httpClient = new HttpClient();\n\t\thttpClient.DefaultRequestHeaders.Add(Messaging.NotificationTunnel.HeaderName, authHash);\n\t\tvar result = httpClient.PostAsJsonAsync(url, payload).Result;\n\t\tif (!result.IsSuccessStatusCode)\n\t\t\tthrow new Exception($\"Problem sending message over notification tunnel: HTTP {result.StatusCode}\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/PopForums.AzureKit.Functions.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\t<PropertyGroup>\n\t\t<Description>PopForums AzureKit Functions</Description>\n\t\t<VersionPrefix>22.0.0</VersionPrefix>\n\t\t<Authors>Jeff Putz</Authors>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<AssemblyName>PopForums.AzureKit.Functions</AssemblyName>\n\t\t<PackageId>PopForums.AzureKit.Functions</PackageId>\n\t\t<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>\n\t\t<PackageProjectUrl>https://github.com/POPWorldMedia/POPForums</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/POPWorldMedia/POPForums</RepositoryUrl>\n\t\t<Copyright>2025, POP World Media, LLC</Copyright>\n\t\t<AzureFunctionsVersion>v4</AzureFunctionsVersion>\n\t\t<OutputType>Exe</OutputType>\n\t</PropertyGroup>\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"10.0.7\" />\n\t\t<PackageReference Include=\"Microsoft.Azure.Functions.Worker.Extensions.Storage\" Version=\"6.8.1\" />\n\t\t<PackageReference Include=\"Microsoft.Azure.Functions.Worker.Extensions.Timer\" Version=\"4.3.1\" />\n\t\t<PackageReference Include=\"Microsoft.Azure.Functions.Worker.Extensions.Http\" Version=\"3.3.0\" />\n\t\t<PackageReference Include=\"Microsoft.Azure.Functions.Worker.Sdk\" Version=\"2.0.7\" OutputItemType=\"Analyzer\" />\n\t\t<PackageReference Include=\"Microsoft.Azure.Functions.Worker\" Version=\"2.52.0\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\PopForums.AzureKit\\PopForums.AzureKit.csproj\" />\n\t\t<ProjectReference Include=\"..\\PopForums.ElasticKit\\PopForums.ElasticKit.csproj\" />\n\t\t<ProjectReference Include=\"..\\PopForums.Sql\\PopForums.Sql.csproj\" />\n\t\t<ProjectReference Include=\"..\\PopForums\\PopForums.csproj\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<None Update=\"host.json\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"local.settings.json\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"local.settings.dev.json\" Condition=\"Exists('local.settings.dev.json')\">\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Update=\"local.settings.dev.json\">\n\t\t  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t  <CopyToPublishDirectory>Never</CopyToPublishDirectory>\n\t\t</None>\n\t</ItemGroup>\n</Project>"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/PostImageCleanupProcessor.cs",
    "content": "﻿using Microsoft.Azure.Functions.Worker;\nusing PopForums.Configuration;\nusing PopForums.Services;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing System;\nusing Microsoft.Extensions.Logging;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class PostImageCleanupProcessor\n{\n\tprivate readonly IPostImageService _postImageService;\n\tprivate readonly IServiceHeartbeatService _serviceHeartbeatService;\n\tprivate readonly IErrorLog _errorLog;\n\n\tpublic PostImageCleanupProcessor(IPostImageService postImageService, IServiceHeartbeatService serviceHeartbeatService, IErrorLog errorLog)\n\t{\n\t\t_postImageService = postImageService;\n\t\t_serviceHeartbeatService = serviceHeartbeatService;\n\t\t_errorLog = errorLog;\n\t}\n\n\t[Function(\"PostImageCleanupProcessor\")]\n\tpublic async Task Run([TimerTrigger(\"0 5 */1 * * *\")] TimerInfo myTimer, FunctionContext executionContext)\n\t{\n\t\tvar logger = executionContext.GetLogger(\"AzureFunction\");\n\t\tvar stopwatch = new Stopwatch();\n\t\tstopwatch.Start();\n\n\t\ttry\n\t\t{\n\t\t\tawait _postImageService.DeleteOldPostImages();\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t\tlogger.LogError(exc, $\"Exception thrown running {nameof(PostImageCleanupProcessor)}\");\n\t\t}\n\n\t\tstopwatch.Stop();\n\t\tlogger.LogInformation($\"C# Timer {nameof(PostImageCleanupProcessor)} function executed ({stopwatch.ElapsedMilliseconds}ms) at: {DateTime.UtcNow}\");\n\t\tawait _serviceHeartbeatService.RecordHeartbeat(typeof(PostImageCleanupProcessor).FullName, \"AzureFunction\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/Program.cs",
    "content": "﻿using System;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing PopForums.Extensions;\nusing PopForums.Sql;\nusing PopForums.Messaging;\nusing PopForums.Configuration;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing PopForums.AzureKit;\nusing PopForums.AzureKit.Functions;\nusing PopForums.ElasticKit;\nusing PopForums.Repositories;\nusing NotificationTunnel = PopForums.AzureKit.Functions.NotificationTunnel;\n\nvar configuration = new ConfigurationBuilder()\n\t.SetBasePath(Environment.CurrentDirectory)\n\t.AddJsonFile(\"local.settings.json\", true)\n\t.AddJsonFile(\"local.settings.dev.json\", true)\n\t.AddEnvironmentVariables()\n\t.Build();\nvar config = new Config(configuration);\n\nvar host = new HostBuilder()\n\t.UseDefaultServiceProvider((_, options) =>\n\t{\n\t\t// there are types not used in functions in core library, so don't choke on them\n\t\toptions.ValidateOnBuild = false;\n\t})\n\t.ConfigureFunctionsWorkerDefaults()\n\t.ConfigureAppConfiguration(c =>\n\t{\n\t\tc.AddConfiguration(configuration);\n\t})\n\t.ConfigureServices(s =>\n\t{\n\t\ts.AddPopForumsBase();\n\t\ts.AddPopForumsSql();\n\t\ts.AddPopForumsAzureFunctionsAndQueues();\n\t\ts.AddSingleton<IBroker, BrokerSink>();\n\t\ts.RemoveAll<ICacheHelper>();\n\t\ts.AddSingleton<ICacheHelper, PopForums.AzureKit.Functions.CacheHelper>();\n\t\ts.RemoveAll<INotificationTunnel>();\n\t\ts.AddTransient<INotificationTunnel, NotificationTunnel>();\n\t\ts.RemoveAll<IPostImageRepository>();\n\t\ts.AddTransient<IPostImageRepository, PopForums.AzureKit.PostImage.PostImageRepository>();\n\n\t\t// use Azure table storage for logging instead of database\n\t\t//s.AddPopForumsTableStorageLogging();\n\n\t\tswitch (config.SearchProvider.ToLower())\n\t\t{\n\t\t\tcase \"elasticsearch\":\n\t\t\tcase \"elasticcloud\":\n\t\t\t\ts.AddPopForumsElasticSearch();\n\t\t\t\tConsole.WriteLine(\"ElasticSearch provider configured.\");\n\t\t\t\tbreak;\n\t\t\tcase \"azuresearch\":\n\t\t\t\ts.AddPopForumsAzureSearch();\n\t\t\t\tConsole.WriteLine(\"Azure Search provider configured.\");\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tConsole.WriteLine(\"Default SQL based search provider configured.\");\n\t\t\t\tbreak;\n\t\t}\n\t})\n\t.Build();\n\nawait host.RunAsync();\n"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/SearchIndexProcessor.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing Microsoft.Azure.Functions.Worker;\nusing Microsoft.Extensions.Logging;\nusing PopForums.AzureKit.Queue;\nusing PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.Services;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class SearchIndexProcessor\n{\n\tprivate readonly ISearchIndexSubsystem _searchIndexSubsystem;\n\tprivate readonly IServiceHeartbeatService _serviceHeartbeatService;\n\tprivate readonly IErrorLog _errorLog;\n\n\tpublic SearchIndexProcessor(ISearchIndexSubsystem searchIndexSubsystem, IServiceHeartbeatService serviceHeartbeatService, IErrorLog errorLog)\n\t{\n\t\t_searchIndexSubsystem = searchIndexSubsystem;\n\t\t_serviceHeartbeatService = serviceHeartbeatService;\n\t\t_errorLog = errorLog;\n\t}\n\n\t[Function(\"SearchIndexProcessor\")]\n\tpublic async Task RunAsync([QueueTrigger(SearchIndexQueueRepository.QueueName)] string jsonPayload, FunctionContext executionContext)\n\t{\n\t\tvar logger = executionContext.GetLogger(\"AzureFunction\");\n\t\tvar stopwatch = new Stopwatch();\n\t\tstopwatch.Start();\n\t\t\n\t\ttry\n\t\t{\n\t\t\tvar payload = JsonSerializer.Deserialize<SearchIndexPayload>(jsonPayload);\n\t\t\t_searchIndexSubsystem.DoIndex(payload.TopicID, payload.TenantID, payload.IsForRemoval);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t\tlogger.LogError(exc, $\"Exception thrown running {nameof(SearchIndexProcessor)}\");\n\t\t}\n\n\t\tstopwatch.Stop();\n\t\tlogger.LogInformation($\"C# Queue SearchIndexProcessor function processed ({stopwatch.ElapsedMilliseconds}ms): {jsonPayload}\");\n\t\tawait _serviceHeartbeatService.RecordHeartbeat(typeof(SearchIndexProcessor).FullName, \"AzureFunction\");\n\t}\n}\n"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/SubscribeNotificationProcessor.cs",
    "content": "﻿using Microsoft.Azure.Functions.Worker;\nusing PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.Services;\nusing System.Diagnostics;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing System;\nusing System.Linq;\nusing Microsoft.Extensions.Logging;\nusing PopForums.AzureKit.Queue;\nusing PopForums.Messaging;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class SubscribeNotificationProcessor\n{\n\tprivate readonly ISubscribedTopicsService _subscribedTopicsService;\n\tprivate readonly IServiceHeartbeatService _serviceHeartbeatService;\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly INotificationTunnel _notificationTunnel;\n\n\tpublic SubscribeNotificationProcessor(ISubscribedTopicsService subscribedTopicsService, IServiceHeartbeatService serviceHeartbeatService, IErrorLog errorLog, INotificationTunnel notificationTunnel)\n\t{\n\t\t_subscribedTopicsService = subscribedTopicsService;\n\t\t_serviceHeartbeatService = serviceHeartbeatService;\n\t\t_errorLog = errorLog;\n\t\t_notificationTunnel = notificationTunnel;\n\t}\n\n\t[Function(\"SubscribeNotificationProcessor\")]\n\tpublic async Task Run([QueueTrigger(SubscribeNotificationRepository.QueueName)] string jsonPayload, FunctionContext executionContext)\n\t{\n\t\tvar logger = executionContext.GetLogger(\"AzureFunction\");\n\t\tvar stopwatch = new Stopwatch();\n\t\tstopwatch.Start();\n\n\t\ttry\n\t\t{\n\t\t\tvar payload = JsonSerializer.Deserialize<SubscribeNotificationPayload>(jsonPayload);\n\t\t\tvar userIDs = await _subscribedTopicsService.GetSubscribedUserIDs(payload.TopicID);\n\t\t\tvar filteredUserIDs = userIDs.Where(x => x != payload.PostingUserID);\n\t\t\tforeach (var userID in filteredUserIDs)\n\t\t\t\t_notificationTunnel.SendNotificationForReply(payload.PostingUserName, payload.TopicTitle, payload.TopicID, userID, payload.TenantID);\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t\tlogger.LogError(exc, $\"Exception thrown running {nameof(SubscribeNotificationProcessor)}\");\n\t\t}\n\n\t\tstopwatch.Stop();\n\t\tlogger.LogInformation($\"C# Queue SubscribeNotificationProcessor function processed ({stopwatch.ElapsedMilliseconds}ms): {jsonPayload}\");\n\t\tawait _serviceHeartbeatService.RecordHeartbeat(typeof(SubscribeNotificationProcessor).FullName, \"AzureFunction\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/UserSessionProcessor.cs",
    "content": "using System;\nusing System.Diagnostics;\nusing System.Threading.Tasks;\nusing Microsoft.Azure.Functions.Worker;\nusing Microsoft.Extensions.Logging;\nusing PopForums.Configuration;\nusing PopForums.Services;\n\nnamespace PopForums.AzureKit.Functions;\n\npublic class UserSessionProcessor\n{\n    private readonly IUserSessionService _userSessionService;\n    private readonly IServiceHeartbeatService _serviceHeartbeatService;\n    private readonly IErrorLog _errorLog;\n\n    public UserSessionProcessor(IUserSessionService userSessionService, IServiceHeartbeatService serviceHeartbeatService, IErrorLog errorLog)\n    {\n\t    _userSessionService = userSessionService;\n\t    _serviceHeartbeatService = serviceHeartbeatService;\n\t    _errorLog = errorLog;\n    }\n\n    [Function(\"UserSessionProcessor\")]\n    public async Task RunAsync([TimerTrigger(\"0 */1 * * * *\")]TimerInfo myTimer, FunctionContext executionContext)\n    {\n        var logger = executionContext.GetLogger(\"AzureFunction\");\n\t\tvar stopwatch = new Stopwatch();\n\t\tstopwatch.Start();\n\n\t\ttry\n\t\t{\n\t\t\tawait _userSessionService.CleanUpExpiredSessions();\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t\tlogger.LogError(exc, $\"Exception thrown running {nameof(UserSessionProcessor)}\");\n\t\t}\n\n\t\tstopwatch.Stop();\n\t\tlogger.LogInformation($\"C# Timer {nameof(UserSessionProcessor)} function executed ({stopwatch.ElapsedMilliseconds}ms) at: {DateTime.UtcNow}\");\n        await _serviceHeartbeatService.RecordHeartbeat(typeof(UserSessionProcessor).FullName, \"AzureFunction\");\n\t}\n}\n"
  },
  {
    "path": "src/PopForums.AzureKit.Functions/host.json",
    "content": "{\n    \"version\": \"2.0\"\n}"
  },
  {
    "path": "src/PopForums.ElasticKit/PopForums.ElasticKit.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n\t  <Description>PopForums ElasticKit Class Library</Description>\n\t\t<VersionPrefix>22.1.0</VersionPrefix>\n\t  <Authors>Jeff Putz</Authors>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t  <AssemblyName>PopForums.ElasticKit</AssemblyName>\n\t  <PackageId>PopForums.ElasticKit</PackageId>\n\t  <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>\n\t  <PackageProjectUrl>https://github.com/POPWorldMedia/POPForums</PackageProjectUrl>\n\t  <RepositoryUrl>https://github.com/POPWorldMedia/POPForums</RepositoryUrl>\n\t  <Copyright>2025, POP World Media, LLC</Copyright>\n\t  <PackageLicenseExpression>MIT</PackageLicenseExpression>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Elastic.Clients.Elasticsearch\" Version=\"9.3.5\" />\n    <PackageReference Include=\"Polly\" Version=\"8.6.6\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\PopForums.Sql\\PopForums.Sql.csproj\" />\n    <ProjectReference Include=\"..\\PopForums\\PopForums.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/PopForums.ElasticKit/Search/ElasticSearchClientWrapper.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Elastic.Clients.Elasticsearch;\nusing Elastic.Clients.Elasticsearch.QueryDsl;\nusing Elastic.Transport;\nusing PopForums.Configuration;\nusing PopForums.Models;\nusing PopForums.Services;\nusing SearchType = PopForums.Models.SearchType;\n\nnamespace PopForums.ElasticKit.Search;\n\npublic interface IElasticSearchClientWrapper\n{\n\tIndexResponse IndexTopic(SearchTopic searchTopic);\n\tResponse<IEnumerable<int>> SearchTopicsWithIDs(string searchTerm, List<int> hiddenForums, SearchType searchType, int startRow, int pageSize, out int topicCount);\n\tvoid VerifyIndexCreate();\n\tDeleteResponse RemoveTopic(string id);\n}\n\npublic class ElasticSearchClientWrapper : IElasticSearchClientWrapper\n{\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly ITenantService _tenantService;\n\tprivate readonly ElasticsearchClient _client;\n\n\tprivate const string IndexName = \"topicindex\";\n\n\tpublic ElasticSearchClientWrapper(IConfig config, IErrorLog errorLog, ITenantService tenantService)\n\t{\n\t\t_errorLog = errorLog;\n\t\t_tenantService = tenantService;\n\t\tElasticsearchClientSettings settings;\n\t\tswitch (config.SearchProvider.ToLower())\n\t\t{\n\t\t\tcase \"elasticsearch\":\n\t\t\t\tsettings = new ElasticsearchClientSettings(new Uri(config.SearchUrl))\n\t\t\t\t\t.DefaultIndex(IndexName).DisableDirectStreaming()\n\t\t\t\t\t.Authentication(new ApiKey(config.SearchKey));\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tsettings = new ElasticsearchClientSettings()\n\t\t\t\t\t.DefaultIndex(IndexName).DisableDirectStreaming();\n\t\t\t\tbreak;\n\t\t}\n\t\t\n\t\t_client = new ElasticsearchClient(settings);\n\t}\n\n\tpublic IndexResponse IndexTopic(SearchTopic searchTopic)\n\t{\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tif (string.IsNullOrWhiteSpace(tenantID))\n\t\t\ttenantID = \"-\";\n\t\tsearchTopic.TenantID = tenantID;\n\t\tvar indexResult = _client.Index(searchTopic);\n\t\treturn indexResult;\n\t}\n\n\tpublic DeleteResponse RemoveTopic(string id)\n\t{\n\t\tvar deleteRequest = new DeleteRequest(IndexName, id);\n\t\tvar response = _client.Delete(deleteRequest);\n\t\treturn response;\n\t}\n\n\tpublic Response<IEnumerable<int>> SearchTopicsWithIDs(string searchTerm, List<int> hiddenForums, SearchType searchType, int startRow, int pageSize, out int topicCount)\n\t{\n\t\tvar sortSelector = new SortOptionsDescriptor<SearchTopic>();\n\t\tswitch (searchType)\n\t\t{\n\t\t\tcase SearchType.Date:\n\t\t\t\tsortSelector.Field(sort => sort.LastPostTime, config => config.Order(SortOrder.Desc));\n\t\t\t\tbreak;\n\t\t\tcase SearchType.Name:\n\t\t\t\tsortSelector.Field(sort => sort.StartedByName, config => config.Order(SortOrder.Asc));\n\t\t\t\tbreak;\n\t\t\tcase SearchType.Replies:\n\t\t\t\tsortSelector.Field(sort => sort.Replies, config => config.Order(SortOrder.Desc));\n\t\t\t\tbreak;\n\t\t\tcase SearchType.Title:\n\t\t\t\tsortSelector.Field(sort => sort.Title, config => config.Order(SortOrder.Asc));\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tsortSelector.Score(config => config.Order(SortOrder.Desc));\n\t\t\t\tbreak;\n\t\t}\n\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tif (string.IsNullOrWhiteSpace(tenantID))\n\t\t\ttenantID = \"-\";\n\t\tstartRow--;\n\t\tvar searchResponse = _client.Search<SearchTopic>(s => s\n\t\t\t.Query(q => q\n\t\t\t\t.Bool(bb => bb\n\t\t\t\t\t.Must(ff => ff\n\t\t\t\t\t\t.MultiMatch(m => m\n\t\t\t\t\t\t\t.Query(searchTerm)\n\t\t\t\t\t\t\t.Fields(new [] { \"title^10\", \"firstPost^5\", \"posts\" })\n\t\t\t\t\t\t\t.Fuzziness(new Fuzziness(\"auto\")))\n\t\t\t\t\t)\n\t\t\t\t\t.MustNot(ff => ff\n\t\t\t\t\t\t.Terms(m => m\n\t\t\t\t\t\t\t.Field(f => f.ForumID)\n\t\t\t\t\t\t\t\t.Terms(new TermsQueryField(hiddenForums.Select(s => (FieldValue)s).ToArray())))\n\t\t\t\t\t)\n\t\t\t\t\t .Filter(ff => ff\n\t\t\t\t\t \t.Term(t => t\n\t\t\t\t\t \t\t.Field(f => f.TenantID).Value(tenantID))\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t\t.SourceIncludes(new []{\"topicID\"})\n\t\t\t.Sort(sortSelector)\n\t\t\t.From(startRow)\n\t\t\t.Size(pageSize));\n\t\tResponse<IEnumerable<int>> result;\n\t\tif (!searchResponse.IsValidResponse)\n\t\t{\n\t\t\tsearchResponse.TryGetOriginalException(out var exception);\n\t\t\t_errorLog.Log(exception, ErrorSeverity.Error, $\"Debugging info: {searchResponse.DebugInformation}\");\n\t\t\tresult = new Response<IEnumerable<int>>(null, false, exception, searchResponse.DebugInformation);\n\t\t\ttopicCount = 0;\n\t\t\treturn result;\n\t\t}\n\t\tvar ids = searchResponse.Documents.Select(d => d.TopicID);\n\t\ttopicCount = (int)searchResponse.Total;\n\t\tresult = new Response<IEnumerable<int>>(ids);\n\t\treturn result;\n\t}\n\n\tpublic void VerifyIndexCreate()\n\t{\n\t\tvar isExists = _client.Indices.Exists(Indices.Index(IndexName)).Exists;\n\t\tif (isExists)\n\t\t\treturn;\n\t\tvar createIndexResponse = _client.Indices.Create(IndexName, c => c\n\t\t\t.Settings(s => s\n\t\t\t\t.Analysis(a => a\n\t\t\t\t\t.Analyzers(aa => aa\n\t\t\t\t\t\t.Standard(\"standard_english\", sa => sa\n\t\t\t\t\t\t\t.Stopwords(new List<string> { \"_english_\" })\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t\t.Mappings(mm => mm\n\t\t\t\t.Properties<SearchTopic>(p => p\n\t\t\t\t\t.Text(t => t.Posts)\n\t\t\t\t\t.Text(t => t.FirstPost)\n\t\t\t\t\t.Text(t => t.Title, tp => tp.Fielddata())\n\t\t\t\t\t.Text(t => t.StartedByName, tp => tp.Fielddata())\n\t\t\t\t\t.Keyword(t => t.TenantID))\n\t\t\t)\n\t\t);\n\t\tif (!createIndexResponse.IsValidResponse)\n\t\t{\n\t\t\tcreateIndexResponse.TryGetOriginalException(out var exception);\n\t\t\t_errorLog.Log(exception, ErrorSeverity.Error, createIndexResponse.DebugInformation);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.ElasticKit/Search/SearchIndexSubsystem.cs",
    "content": "﻿using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Elastic.Clients.Elasticsearch;\nusing Polly;\nusing Polly.Retry;\nusing PopForums.Configuration;\nusing PopForums.Services;\n\nnamespace PopForums.ElasticKit.Search;\n\npublic class SearchIndexSubsystem : ISearchIndexSubsystem\n{\n\tprivate readonly ITextParsingService _textParsingService;\n\tprivate readonly IPostService _postService;\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly IElasticSearchClientWrapper _elasticSearchClientWrapper;\n\n\tpublic SearchIndexSubsystem(ITextParsingService textParsingService, IPostService postService, ITopicService topicService, IErrorLog errorLog, IElasticSearchClientWrapper elasticSearchClientWrapper)\n\t{\n\t\t_textParsingService = textParsingService;\n\t\t_postService = postService;\n\t\t_topicService = topicService;\n\t\t_errorLog = errorLog;\n\t\t_elasticSearchClientWrapper = elasticSearchClientWrapper;\n\t}\n\n\tpublic void DoIndex(int topicID, string tenantID, bool isForRemoval)\n\t{\n\t\tif (isForRemoval)\n\t\t{\n\t\t\tRemoveIndex(topicID, tenantID);\n\t\t\treturn;\n\t\t}\n\n\t\tvar topic = _topicService.Get(topicID).Result;\n\t\tif (topic == null)\n\t\t\treturn;\n\n\t\t_elasticSearchClientWrapper.VerifyIndexCreate();\n\n\t\tvar posts = _postService.GetPosts(topic, false).Result;\n\t\tif (posts.Count == 0)\n\t\t\tthrow new Exception($\"TopicID {topic.TopicID} has no posts to index.\");\n\t\tvar firstPost = _textParsingService.ClientHtmlToForumCode(posts[0].FullText);\n\t\tfirstPost = _textParsingService.RemoveForumCode(firstPost);\n\t\tposts.RemoveAt(0);\n\t\tvar parsedPosts = posts.Select(x =>\n\t\t{\n\t\t\tvar parsedText = _textParsingService.ClientHtmlToForumCode(x.FullText);\n\t\t\tparsedText = _textParsingService.RemoveForumCode(parsedText);\n\t\t\treturn parsedText;\n\t\t}).ToArray();\n\t\tvar searchTopic = new SearchTopic\n\t\t{\n\t\t\tId = $\"{tenantID}-{topic.TopicID}\",\n\t\t\tTopicID = topic.TopicID,\n\t\t\tForumID = topic.ForumID,\n\t\t\tTitle = topic.Title,\n\t\t\tLastPostTime = topic.LastPostTime,\n\t\t\tStartedByName = topic.StartedByName,\n\t\t\tReplies = topic.ReplyCount,\n\t\t\tViews = topic.ViewCount,\n\t\t\tIsClosed = topic.IsClosed,\n\t\t\tIsPinned = topic.IsPinned,\n\t\t\tUrlName = topic.UrlName,\n\t\t\tLastPostName = topic.LastPostName,\n\t\t\tFirstPost = firstPost,\n\t\t\tPosts = parsedPosts,\n\t\t\tTenantID = tenantID\n\t\t};\n\n\t\ttry\n\t\t{\n\t\t\tvar pipeline = new ResiliencePipelineBuilder<IndexResponse>()\n\t\t\t\t.AddRetry(new RetryStrategyOptions<IndexResponse>\n\t\t\t\t{\n\t\t\t\t\tShouldHandle = new PredicateBuilder<IndexResponse>()\n\t\t\t\t\t\t.HandleResult(static result => !result.IsValidResponse),\n\t\t\t\t\tDelayGenerator = static args =>\n\t\t\t\t\t{\n\t\t\t\t\t\tvar delay = args.AttemptNumber switch\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t0 => TimeSpan.FromSeconds(1),\n\t\t\t\t\t\t\t1 => TimeSpan.FromSeconds(5),\n\t\t\t\t\t\t\t_ => TimeSpan.FromSeconds(30)\n\t\t\t\t\t\t};\n\t\t\t\t\t\treturn new ValueTask<TimeSpan?>(delay);\n\t\t\t\t\t},\n\t\t\t\t\tOnRetry = responseArgs =>\n\t\t\t\t\t{\n\t\t\t\t\t\t_errorLog.Log(responseArgs.Outcome.Exception, ErrorSeverity.Error,\n\t\t\t\t\t\t\t$\"Retry after {responseArgs.Duration.Seconds}: {responseArgs.Outcome.Result?.DebugInformation}\");\n\t\t\t\t\t\treturn default;\n\t\t\t\t\t}\n\t\t\t\t}).Build();\n\t\t\t\n\t\t\tpipeline.Execute(() =>\n\t\t\t{\n\t\t\t\tvar indexResult = _elasticSearchClientWrapper.IndexTopic(searchTopic);\n\t\t\t\treturn indexResult;\n\t\t\t});\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n\n\tpublic void RemoveIndex(int topicID, string tenantID)\n\t{\n\t\tvar id = $\"{tenantID}-{topicID}\";\n\n\t\ttry\n\t\t{\n\t\t\tvar result = _elasticSearchClientWrapper.RemoveTopic(id);\n\t\t\tif (result.Result != Result.Deleted)\n\t\t\t{\n\t\t\t\tresult.TryGetOriginalException(out var exc);\n\t\t\t\t_errorLog.Log(exc, ErrorSeverity.Error, $\"Debug information: {result.DebugInformation}\");\n\t\t\t}\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t_errorLog.Log(exc, ErrorSeverity.Error);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.ElasticKit/Search/SearchRepository.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing PopForums.Models;\nusing PopForums.Repositories;\nusing PopForums.Sql;\n#pragma warning disable 1998\n\nnamespace PopForums.ElasticKit.Search;\n\npublic class SearchRepository : Sql.Repositories.SearchRepository\n{\n\tprivate readonly ITopicRepository _topicRepository;\n\tprivate readonly IElasticSearchClientWrapper _elasticSearchClientWrapper;\n\n\tpublic SearchRepository(ISqlObjectFactory sqlObjectFactory, ITopicRepository topicRepository, IElasticSearchClientWrapper elasticSearchClientWrapper) : base(sqlObjectFactory)\n\t{\n\t\t_topicRepository = topicRepository;\n\t\t_elasticSearchClientWrapper = elasticSearchClientWrapper;\n\t}\n\n\tpublic override async Task<List<string>> GetJunkWords()\n\t{\n\t\treturn new List<string>();\n\t}\n\n\tpublic override async Task CreateJunkWord(string word)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic override async Task DeleteJunkWord(string word)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic override async Task DeleteAllIndexedWordsForTopic(int topicID)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic override async Task SaveSearchWord(int topicID, string word, int rank)\n\t{\n\t\tthrow new NotImplementedException();\n\t}\n\n\tpublic override async Task<Tuple<Response<List<Topic>>, int>> SearchTopics(string searchTerm, List<int> hiddenForums, SearchType searchType, int startRow, int pageSize)\n\t{\n\t\tvar response = _elasticSearchClientWrapper.SearchTopicsWithIDs(searchTerm, hiddenForums, searchType, startRow, pageSize, out var topicCount);\n\t\tResponse<List<Topic>> result;\n\t\tif (!response.IsValid)\n\t\t{\n\t\t\tresult = new Response<List<Topic>>(null, false, response.Exception, response.DebugInfo);\n\t\t\treturn Tuple.Create(result, topicCount);\n\t\t}\n\t\tvar topics = await _topicRepository.Get(response.Data);\n\t\tresult = new Response<List<Topic>>(topics);\n\t\treturn Tuple.Create(result, topicCount);\n\t}\n}"
  },
  {
    "path": "src/PopForums.ElasticKit/Search/SearchTopic.cs",
    "content": "﻿using System;\n\nnamespace PopForums.ElasticKit.Search;\n\npublic class SearchTopic\n{\n\tpublic string Id { get; set; }\n\tpublic int TopicID { get; set; }\n\tpublic int ForumID { get; set; }\n\tpublic string Title { get; set; }\n\tpublic DateTime LastPostTime { get; set; }\n\tpublic string StartedByName { get; set; }\n\tpublic int Replies { get; set; }\n\tpublic int Views { get; set; }\n\tpublic bool IsClosed { get; set; }\n\tpublic bool IsPinned { get; set; }\n\tpublic string UrlName { get; set; }\n\tpublic string LastPostName { get; set; }\n\tpublic string FirstPost { get; set; }\n\tpublic string[] Posts { get; set; }\n\tpublic string TenantID { get; set; }\n}"
  },
  {
    "path": "src/PopForums.ElasticKit/ServiceCollectionExtensions.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing PopForums.ElasticKit.Search;\nusing PopForums.Repositories;\nusing PopForums.Services;\nusing SearchIndexSubsystem = PopForums.ElasticKit.Search.SearchIndexSubsystem;\n\nnamespace PopForums.ElasticKit;\n\npublic static class ServiceCollectionExtensions\n{\n\tpublic static IServiceCollection AddPopForumsElasticSearch(this IServiceCollection services)\n\t{\n\t\tservices.Replace(ServiceDescriptor.Transient<ISearchRepository, SearchRepository>());\n\t\tservices.Replace(ServiceDescriptor.Transient<ISearchIndexSubsystem, SearchIndexSubsystem>());\n\t\tservices.AddTransient<IElasticSearchClientWrapper, ElasticSearchClientWrapper>();\n\t\treturn services;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Authentication/PopForumsAuthenticationDefaults.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Authentication;\n\npublic static class PopForumsAuthenticationDefaults\n{\n\tpublic const string AuthenticationScheme = \"PopForumsAuthScheme\";\n\tpublic const string ForumsClaimType = \"http://popforums.com/forumclaims\";\n\tpublic const string ForumsUserIDType = \"http://popforums.com/forumuserid\";\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Authentication/PopForumsAuthenticationIgnoreAttribute.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Authentication;\n\npublic class PopForumsAuthenticationIgnoreAttribute : Attribute\n{\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Authentication/PopForumsAuthenticationMiddleware.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Authentication;\n\npublic class PopForumsAuthenticationMiddleware(RequestDelegate next)\n{\n\tpublic async Task InvokeAsync(HttpContext context, IUserService userService, IProfileService profileService, ISetupService setupService, IConfig config, IOAuthOnlyService oAuthOnlyService)\n\t{\n\t\tvar isSetupAndConnectionGood = setupService.IsRuntimeConnectionAndSetupGood();\n\t\tif (!isSetupAndConnectionGood)\n\t\t{\n\t\t\tawait next.Invoke(context);\n\t\t\treturn;\n\t\t}\n\n\t\tvar endpoint = context.GetEndpoint();\n\t\tif (endpoint?.Metadata.GetMetadata<PopForumsAuthenticationIgnoreAttribute>() is not null)\n\t\t{\n\t\t\tawait next.Invoke(context);\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tvar authResult = await context.AuthenticateAsync(PopForumsAuthenticationDefaults.AuthenticationScheme);\n\t\tif (authResult.Principal?.Identity is ClaimsIdentity identity)\n\t\t{\n\t\t\tvar user = await userService.GetUserByName(identity.Name);\n\t\t\tif (user != null)\n\t\t\t{\n\t\t\t\tif (user.Roles != null && user.Roles.Any())\n\t\t\t\t\tforeach (var role in user.Roles)\n\t\t\t\t\t\tidentity.AddClaim(new Claim(PopForumsAuthenticationDefaults.ForumsClaimType, role));\n\t\t\t\tidentity.AddClaim(new Claim(PopForumsAuthenticationDefaults.ForumsUserIDType, user.UserID.ToString()));\n\t\t\t\tcontext.Items[\"PopForumsUser\"] = user;\n\t\t\t\tvar profile = await profileService.GetProfile(user);\n\t\t\t\tcontext.Items[\"PopForumsProfile\"] = profile;\n\t\t\t\tcontext.User = new ClaimsPrincipal(identity);\n\t\t\t\tif (config.IsOAuthOnly && user.TokenExpiration < DateTime.UtcNow)\n\t\t\t\t{\n\t\t\t\t\tvar isSuccess = await oAuthOnlyService.AttemptTokenRefresh(user);\n\t\t\t\t\tif (!isSuccess)\n\t\t\t\t\t\tcontext.Response.Redirect(\"/Forums/Account/Login\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tawait next.Invoke(context);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Authorization/OAuthOnlyForbidAttribute.cs",
    "content": "namespace PopForums.Mvc.Areas.Forums.Authorization;\n\npublic class OAuthOnlyForbidAttribute(IConfig config) : Attribute, IResourceFilter\n{\n\tpublic void OnResourceExecuting(ResourceExecutingContext context)\n\t{\n\t\tif (config.IsOAuthOnly)\n\t\t\tcontext.Result = new ForbidResult();\n\t}\n\n\tpublic void OnResourceExecuted(ResourceExecutedContext context)\n\t{\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Authorization/PopForumsPrivateForumsFilter.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Authorization;\n\npublic class PopForumsPrivateForumsFilter(IUserRetrievalShim userRetrievalShim, ISettingsManager settingsManager, IConfig config) : IActionFilter\n{\n\tpublic void OnActionExecuting(ActionExecutingContext context)\n\t{\n\t\tif (!settingsManager.Current.IsPrivateForumInstance && !config.IsOAuthOnly)\n\t\t\treturn;\n\t\tif (userRetrievalShim.GetUser() == null)\n\t\t\tcontext.Result = new RedirectToActionResult(\"Login\", AccountController.Name, null);\n\t}\n\n\tpublic void OnActionExecuted(ActionExecutedContext context)\n\t{\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Authorization/PopForumsUserAttribute.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Authorization;\n\n/// <summary>\n/// This attribute, typically applied globally, is used to track sessions for all users (authenticated or not)\n/// and drive the \"currently online\" list and user count.\n/// </summary>\n/// <param name=\"userSessionService\"></param>\npublic class PopForumsUserAttribute(IUserSessionService userSessionService) : IAuthorizationFilter, IAsyncActionFilter\n{\n\tprivate bool _ignore;\n\n\tprotected virtual bool IsGlobalFilter()\n\t{\n\t\treturn false;\n\t}\n\n\tpublic void OnAuthorization(AuthorizationFilterContext context)\n\t{\n\t\tvar controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;\n\t\tif (controllerActionDescriptor == null)\n\t\t\treturn;\n\t\tif (!IsValidToRunOnController(controllerActionDescriptor.ControllerTypeInfo))\n\t\t{\n\t\t\t_ignore = true;\n\t\t\treturn;\n\t\t}\n\t\tvar attributes = controllerActionDescriptor.MethodInfo.GetCustomAttributes(typeof(PopForumsAuthenticationIgnoreAttribute), false);\n\t\tif (attributes.Any())\n\t\t{\n\t\t\t_ignore = true;\n\t\t\treturn;\n\t\t}\n\t\t_ignore = false;\n\t}\n\n\tpublic async Task OnActionExecutionAsync(ActionExecutingContext filterContext, ActionExecutionDelegate next)\n\t{\n\t\tif (_ignore)\n\t\t{\n\t\t\tawait next.Invoke();\n\t\t\treturn;\n\t\t}\n\n\t\tvar userAgents = filterContext.HttpContext.Request.Headers.UserAgent;\n\t\tif (userAgents.Count > 0 && (userAgents[0].ToLower().Contains(\"bot\") || userAgents[0].ToLower().Contains(\"crawl\")))\n\t\t{\n\t\t\tawait next.Invoke();\n\t\t\treturn;\n\t\t}\n\n\t\tif (filterContext.HttpContext.Response.StatusCode == StatusCodes.Status301MovedPermanently || filterContext.HttpContext.Response.StatusCode == StatusCodes.Status302Found)\n\t\t{\n\t\t\tawait next.Invoke();\n\t\t\treturn;\n\t\t}\n\t\tint.TryParse(filterContext.HttpContext.Request.Cookies[UserSessionService._sessionIDCookieName], out var cookieSessionID);\n\t\tvar sessionID = cookieSessionID == 0 ? (int?)null : cookieSessionID;\n\t\tvar user = filterContext.HttpContext.Items[\"PopForumsUser\"] as User;\n\t\tawait userSessionService.ProcessUserRequest(user, sessionID, filterContext.HttpContext.Connection.RemoteIpAddress.ToString(), \n\t\t\t() => filterContext.HttpContext.Response.Cookies.Delete(UserSessionService._sessionIDCookieName), \n\t\t\ts => filterContext.HttpContext.Response.Cookies.Append(UserSessionService._sessionIDCookieName, s.ToString()));\n\t\tawait next.Invoke();\n\t}\n\n\tprivate bool IsValidToRunOnController(TypeInfo controllerType)\n\t{\n\t\tif (IsGlobalFilter())\n\t\t\treturn true;\n\t\tvar controllerNamespace = controllerType.Namespace;\n\t\treturn controllerNamespace != null && controllerNamespace.StartsWith(\"PopForums\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/BackgroundJobs/AwardCalculatorJob.cs",
    "content": "using System.Threading;\nusing Microsoft.Extensions.Hosting;\n\nnamespace PopForums.Mvc.Areas.Forums.BackgroundJobs;\n\npublic class AwardCalculatorJob(ISettingsManager settingsManager, IServiceHeartbeatService serviceHeartbeatService, IAwardCalculatorWorker awardCalculatorWorker, IServiceProvider serviceProvider) : BackgroundService\n{\n\tprotected override async Task ExecuteAsync(CancellationToken stoppingToken)\n\t{\n\t\tPeriodicTimer timer;\n\t\ttry\n\t\t{\n\t\t\ttimer = new(TimeSpan.FromMilliseconds(settingsManager.Current.ScoringGameCalculatorInterval));\n\t\t}\n\t\tcatch(Exception ex)\n\t\t{\n\t\t\tvar logger = await GetLogger();\n\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job. This job will not restart without restarting the app.\");\n\t\t\treturn;\n\t\t}\n\t\t\n\t\twhile (!stoppingToken.IsCancellationRequested)\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tawardCalculatorWorker.Execute();\n\t\t\t\tawait serviceHeartbeatService.RecordHeartbeat(GetType().FullName, Environment.MachineName);\n\t\t\t\ttimer.Period = TimeSpan.FromMilliseconds(settingsManager.Current.ScoringGameCalculatorInterval);\n\t\t\t}\n\t\t\tcatch (Exception ex)\n\t\t\t{\n\t\t\t\tvar logger = await GetLogger();\n\t\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job.\");\n\t\t\t}\n\t\t\tawait timer.WaitForNextTickAsync(stoppingToken);\n\t\t}\n\t}\n\n\tprivate async Task<ILogger<AwardCalculatorJob>> GetLogger()\n\t{\n\t\tawait using var scope = serviceProvider.CreateAsyncScope();\n\t\tvar logger = scope.ServiceProvider.GetRequiredService<ILogger<AwardCalculatorJob>>();\n\t\treturn logger;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/BackgroundJobs/CloseAgedTopicsJob.cs",
    "content": "using System.Threading;\nusing Microsoft.Extensions.Hosting;\n\nnamespace PopForums.Mvc.Areas.Forums.BackgroundJobs;\n\npublic class CloseAgedTopicsJob(IServiceHeartbeatService serviceHeartbeatService, ICloseAgedTopicsWorker closeAgedTopicsWorker, IServiceProvider serviceProvider) : BackgroundService\n{\n\tprivate const int IntervalValue = 12;\n\tprivate readonly PeriodicTimer _timer = new(TimeSpan.FromHours(IntervalValue));\n\t\n\tprotected override async Task ExecuteAsync(CancellationToken stoppingToken)\n\t{\n\t\twhile (!stoppingToken.IsCancellationRequested)\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tcloseAgedTopicsWorker.Execute();\n\t\t\t\tawait serviceHeartbeatService.RecordHeartbeat(GetType().FullName, Environment.MachineName);\n\t\t\t}\n\t\t\tcatch (Exception ex)\n\t\t\t{\n\t\t\t\tvar logger = await GetLogger();\n\t\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job.\");\n\t\t\t}\n\t\t\tawait _timer.WaitForNextTickAsync(stoppingToken);\n\t\t}\n\t}\n\n\tprivate async Task<ILogger<CloseAgedTopicsJob>> GetLogger()\n\t{\n\t\tawait using var scope = serviceProvider.CreateAsyncScope();\n\t\tvar logger = scope.ServiceProvider.GetRequiredService<ILogger<CloseAgedTopicsJob>>();\n\t\treturn logger;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/BackgroundJobs/EmailJob.cs",
    "content": "using System.Threading;\nusing Microsoft.Extensions.Hosting;\n\nnamespace PopForums.Mvc.Areas.Forums.BackgroundJobs;\n\npublic class EmailJob(ISettingsManager settingsManager, IServiceHeartbeatService serviceHeartbeatService, IEmailWorker emailWorker, IServiceProvider serviceProvider) : BackgroundService\n{\n\tprotected override async Task ExecuteAsync(CancellationToken stoppingToken)\n\t{\n\t\tPeriodicTimer timer;\n\t\ttry\n\t\t{\n\t\t\ttimer = new(TimeSpan.FromMilliseconds(settingsManager.Current.MailSendingInverval));\n\t\t}\n\t\tcatch(Exception ex)\n\t\t{\n\t\t\tvar logger = await GetLogger();\n\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job. This job will not restart without restarting the app.\");\n\t\t\treturn;\n\t\t}\n\t\twhile (!stoppingToken.IsCancellationRequested)\n\t\t{\n\t\t\ttry\n\t\t\t{\n#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed\n\t\t\t\temailWorker.Execute();\n#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed\n\t\t\t\tawait serviceHeartbeatService.RecordHeartbeat(GetType().FullName, Environment.MachineName);\n\t\t\t\tvar newTimeSpan = TimeSpan.FromMilliseconds(settingsManager.Current.MailSendingInverval);\n\t\t\t\ttimer.Period = newTimeSpan;\n\t\t\t}\n\t\t\tcatch (Exception ex)\n\t\t\t{\n\t\t\t\tvar logger = await GetLogger();\n\t\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job.\");\n\t\t\t}\n\t\t\tawait timer.WaitForNextTickAsync(stoppingToken);\n\t\t}\n\t}\n\n\tprivate async Task<ILogger<EmailJob>> GetLogger()\n\t{\n\t\tawait using var scope = serviceProvider.CreateAsyncScope();\n\t\tvar logger = scope.ServiceProvider.GetRequiredService<ILogger<EmailJob>>();\n\t\treturn logger;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/BackgroundJobs/PostImageCleanupJob.cs",
    "content": "using System.Threading;\nusing Microsoft.Extensions.Hosting;\n\nnamespace PopForums.Mvc.Areas.Forums.BackgroundJobs;\n\npublic class PostImageCleanupJob(IServiceHeartbeatService serviceHeartbeatService, IPostImageCleanupWorker postImageCleanupWorker, IServiceProvider serviceProvider) : BackgroundService\n{\n\tprivate const double IntervalValue = 12;\n\tprivate readonly PeriodicTimer _timer = new(TimeSpan.FromHours(IntervalValue));\n\t\n\tprotected override async Task ExecuteAsync(CancellationToken stoppingToken)\n\t{\n\t\twhile (!stoppingToken.IsCancellationRequested)\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tpostImageCleanupWorker.Execute();\n\t\t\t\tawait serviceHeartbeatService.RecordHeartbeat(GetType().FullName, Environment.MachineName);\n\t\t\t}\n\t\t\tcatch (Exception ex)\n\t\t\t{\n\t\t\t\tvar logger = await GetLogger();\n\t\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job.\");\n\t\t\t}\n\t\t\tawait _timer.WaitForNextTickAsync(stoppingToken);\n\t\t}\n\t}\n\n\tprivate async Task<ILogger<PostImageCleanupJob>> GetLogger()\n\t{\n\t\tawait using var scope = serviceProvider.CreateAsyncScope();\n\t\tvar logger = scope.ServiceProvider.GetRequiredService<ILogger<PostImageCleanupJob>>();\n\t\treturn logger;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/BackgroundJobs/SearchIndexJob.cs",
    "content": "using System.Threading;\nusing Microsoft.Extensions.Hosting;\n\nnamespace PopForums.Mvc.Areas.Forums.BackgroundJobs;\n\npublic class SearchIndexJob(ISettingsManager settingsManager, IServiceHeartbeatService serviceHeartbeatService, ISearchIndexWorker searchIndexWorker, IServiceProvider serviceProvider) : BackgroundService\n{\n\tprotected override async Task ExecuteAsync(CancellationToken stoppingToken)\n\t{\n\t\tPeriodicTimer timer;\n\t\ttry\n\t\t{\n\t\t\ttimer = new(TimeSpan.FromMilliseconds(settingsManager.Current.SearchIndexingInterval));\n\t\t}\n\t\tcatch(Exception ex)\n\t\t{\n\t\t\tvar logger = await GetLogger();\n\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job. This job will not restart without restarting the app.\");\n\t\t\treturn;\n\t\t}\n\t\twhile (!stoppingToken.IsCancellationRequested)\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tsearchIndexWorker.Execute();\n\t\t\t\tawait serviceHeartbeatService.RecordHeartbeat(GetType().FullName, Environment.MachineName);\n\t\t\t\ttimer.Period = TimeSpan.FromMilliseconds(settingsManager.Current.SearchIndexingInterval);\n\t\t\t}\n\t\t\tcatch (Exception ex)\n\t\t\t{\n\t\t\t\tvar logger = await GetLogger();\n\t\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job.\");\n\t\t\t}\n\t\t\tawait timer.WaitForNextTickAsync(stoppingToken);\n\t\t}\n\t}\n\n\tprivate async Task<ILogger<SearchIndexJob>> GetLogger()\n\t{\n\t\tawait using var scope = serviceProvider.CreateAsyncScope();\n\t\tvar logger = scope.ServiceProvider.GetRequiredService<ILogger<SearchIndexJob>>();\n\t\treturn logger;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/BackgroundJobs/SubscribeNotificationJob.cs",
    "content": "using System.Threading;\nusing Microsoft.Extensions.Hosting;\n\nnamespace PopForums.Mvc.Areas.Forums.BackgroundJobs;\n\npublic class SubscribeNotificationJob(IServiceHeartbeatService serviceHeartbeatService, ISubscribeNotificationWorker subscribeNotificationWorker, IServiceProvider serviceProvider) : BackgroundService\n{\n\tprivate const double IntervalValue = 15;\n\tprivate readonly PeriodicTimer _timer = new(TimeSpan.FromSeconds(IntervalValue));\n\t\n\tprotected override async Task ExecuteAsync(CancellationToken stoppingToken)\n\t{\n\t\twhile (!stoppingToken.IsCancellationRequested)\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tsubscribeNotificationWorker.Execute();\n\t\t\t\tawait serviceHeartbeatService.RecordHeartbeat(GetType().FullName, Environment.MachineName);\n\t\t\t}\n\t\t\tcatch (Exception ex)\n\t\t\t{\n\t\t\t\tvar logger = await GetLogger();\n\t\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job.\");\n\t\t\t}\n\t\t\tawait _timer.WaitForNextTickAsync(stoppingToken);\n\t\t}\n\t}\n\n\tprivate async Task<ILogger<PostImageCleanupJob>> GetLogger()\n\t{\n\t\tawait using var scope = serviceProvider.CreateAsyncScope();\n\t\tvar logger = scope.ServiceProvider.GetRequiredService<ILogger<PostImageCleanupJob>>();\n\t\treturn logger;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/BackgroundJobs/UserSessionJob.cs",
    "content": "using System.Threading;\nusing Microsoft.Extensions.Hosting;\n\nnamespace PopForums.Mvc.Areas.Forums.BackgroundJobs;\n\npublic class UserSessionJob(IServiceHeartbeatService serviceHeartbeatService, IUserSessionWorker userSessionWorker, IServiceProvider serviceProvider) : BackgroundService\n{\n\tprivate const double IntervalValue = 1;\n\tprivate readonly PeriodicTimer _timer = new(TimeSpan.FromMinutes(IntervalValue));\n\t\n\tprotected override async Task ExecuteAsync(CancellationToken stoppingToken)\n\t{\n\t\twhile (!stoppingToken.IsCancellationRequested)\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tuserSessionWorker.Execute();\n\t\t\t\tawait serviceHeartbeatService.RecordHeartbeat(GetType().FullName, Environment.MachineName);\n\t\t\t}\n\t\t\tcatch (Exception ex)\n\t\t\t{\n\t\t\t\tvar logger = await GetLogger();\n\t\t\t\tlogger.LogError(ex, $\"Error while executing {GetType().FullName} background job.\");\n\t\t\t}\n\t\t\tawait _timer.WaitForNextTickAsync(stoppingToken);\n\t\t}\n\t}\n\n\tprivate async Task<ILogger<UserSessionJob>> GetLogger()\n\t{\n\t\tawait using var scope = serviceProvider.CreateAsyncScope();\n\t\tvar logger = scope.ServiceProvider.GetRequiredService<ILogger<UserSessionJob>>();\n\t\treturn logger;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/AccountController.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\nusing PopIdentity;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\npublic class AccountController : Controller\n{\n\tpublic AccountController(IUserService userService, IProfileService profileService, INewAccountMailer newAccountMailer, ISettingsManager settingsManager, IPostService postService, ITopicService topicService, IForumService forumService, ILastReadService lastReadService, IImageService imageService, IFeedService feedService, IUserAwardService userAwardService, IExternalUserAssociationManager externalUserAssociationManager, IUserRetrievalShim userRetrievalShim, IExternalLoginRoutingService externalLoginRoutingService, IExternalLoginTempService externalLoginTempService, IConfig config, IReCaptchaService reCaptchaService, IOAuthOnlyService oAuthOnlyService)\n\t{\n\t\t_userService = userService;\n\t\t_settingsManager = settingsManager;\n\t\t_profileService = profileService;\n\t\t_newAccountMailer = newAccountMailer;\n\t\t_postService = postService;\n\t\t_topicService = topicService;\n\t\t_forumService = forumService;\n\t\t_lastReadService = lastReadService;\n\t\t_imageService = imageService;\n\t\t_feedService = feedService;\n\t\t_userAwardService = userAwardService;\n\t\t_externalUserAssociationManager = externalUserAssociationManager;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_externalLoginRoutingService = externalLoginRoutingService;\n\t\t_externalLoginTempService = externalLoginTempService;\n\t\t_config = config;\n\t\t_reCaptchaService = reCaptchaService;\n\t\t_oAuthOnlyService = oAuthOnlyService;\n\t}\n\n\tpublic static string Name = \"Account\";\n\tpublic static string CoppaDateKey = \"CoppaDateKey\";\n\tpublic static string TosKey = \"TosKey\";\n\n\tprivate readonly IUserService _userService;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IProfileService _profileService;\n\tprivate readonly INewAccountMailer _newAccountMailer;\n\tprivate readonly IPostService _postService;\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly ILastReadService _lastReadService;\n\tprivate readonly IImageService _imageService;\n\tprivate readonly IFeedService _feedService;\n\tprivate readonly IUserAwardService _userAwardService;\n\tprivate readonly IExternalUserAssociationManager _externalUserAssociationManager;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly IExternalLoginRoutingService _externalLoginRoutingService;\n\tprivate readonly IExternalLoginTempService _externalLoginTempService;\n\tprivate readonly IConfig _config;\n\tprivate readonly IReCaptchaService _reCaptchaService;\n\tprivate readonly IOAuthOnlyService _oAuthOnlyService;\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\tpublic IActionResult Create()\n\t{\n\t\tSetupCreateData();\n\t\tvar signupData = new SignupData\n\t\t{\n\t\t\tIsSubscribed = true,\n\t\t\tIsAutoFollowOnReply = true\n\t\t};\n\t\tvar loginState = _externalLoginTempService.Read();\n\t\tif (loginState?.ResultData != null)\n\t\t{\n\t\t\tsignupData.Email = loginState.ResultData.Email;\n\t\t\tsignupData.Name = loginState.ResultData.Name;\n\t\t}\n\t\treturn View(signupData);\n\t}\n\n\tprivate void SetupCreateData()\n\t{\n\t\tViewData[CoppaDateKey] = SignupData.GetCoppaDate();\n\t\tViewData[TosKey] = _settingsManager.Current.TermsOfService;\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic async Task<IActionResult> Create(SignupData signupData)\n\t{\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress.ToString();\n\t\tif (_config.UseReCaptcha)\n\t\t{\n\t\t\tvar reCaptchaResponse = await _reCaptchaService.VerifyToken(signupData.Token, ip);\n\t\t\tif (!reCaptchaResponse.IsSuccess)\n\t\t\t\tModelState.AddModelError(\"Email\", Resources.BotError);\n\t\t}\n\t\tawait ValidateSignupData(signupData, ModelState, ip);\n\t\tif (ModelState.IsValid)\n\t\t{\n\t\t\tvar user = await _userService.CreateUserWithProfile(signupData, ip);\n\t\t\tvar verifyUrl = Url.Action(\"Verify\", \"Account\", null, Request.Scheme);\n\t\t\tvar result = _newAccountMailer.Send(user, verifyUrl);\n\t\t\tif (result != SmtpStatusCode.Ok)\n\t\t\t\tViewData[\"EmailProblem\"] = Resources.EmailProblemAccount + (result?.ToString() ?? \"App exception\") + \".\";\n\t\t\tif (_settingsManager.Current.IsNewUserApproved)\n\t\t\t{\n\t\t\t\tViewData[\"Result\"] = Resources.AccountReady;\n\t\t\t\tawait _userService.Login(user, ip);\n\t\t\t}\n\t\t\telse\n\t\t\t\tViewData[\"Result\"] = Resources.AccountReadyCheckEmail;\n\n\t\t\tvar loginState = _externalLoginTempService.Read();\n\t\t\tif (loginState != null)\n\t\t\t{\n\t\t\t\tvar externalLoginInfo = new ExternalLoginInfo(loginState.ProviderType.ToString(), loginState.ResultData.ID, loginState.ResultData.Name);\n\t\t\t\tawait _externalUserAssociationManager.Associate(user, externalLoginInfo, ip);\n\t\t\t}\n\n\t\t\tawait IdentityController.PerformSignInAsync(user, HttpContext);\n\n\t\t\treturn View(\"AccountCreated\");\n\t\t}\n\t\tSetupCreateData();\n\t\treturn View(signupData);\n\t}\n\n\tprivate async Task ValidateSignupData(SignupData signupData, ModelStateDictionary modelState, string ip)\n\t{\n\t\tif (!signupData.IsCoppa)\n\t\t\tmodelState.AddModelError(\"IsCoppa\", Resources.MustBe13);\n\t\tif (!signupData.IsTos)\n\t\t\tmodelState.AddModelError(\"IsTos\", Resources.MustAcceptTOS);\n\t\tvar passwordValid = _userService.IsPasswordValid(signupData.Password, out var passwordError);\n\t\tif (!passwordValid)\n\t\t\tmodelState.AddModelError(\"Password\", passwordError);\n\t\tif (signupData.Password != signupData.PasswordRetype)\n\t\t\tmodelState.AddModelError(\"PasswordRetype\", Resources.RetypeYourPassword);\n\t\tif (string.IsNullOrWhiteSpace(signupData.Name))\n\t\t\tmodelState.AddModelError(\"Name\", Resources.NameRequired);\n\t\telse if (await _userService.IsNameInUse(signupData.Name))\n\t\t\tmodelState.AddModelError(\"Name\", Resources.NameInUse);\n\t\tif (string.IsNullOrWhiteSpace(signupData.Email))\n\t\t\tmodelState.AddModelError(\"Email\", Resources.EmailRequired);\n\t\telse\n\t\tif (!signupData.Email.IsEmailAddress())\n\t\t\tmodelState.AddModelError(\"Email\", Resources.ValidEmailAddressRequired);\n\t\telse if (signupData.Email != null && await _userService.IsEmailInUse(signupData.Email))\n\t\t\tmodelState.AddModelError(\"Email\", Resources.EmailInUse);\n\t\tif (signupData.Email != null && await _userService.IsEmailBanned(signupData.Email))\n\t\t\tmodelState.AddModelError(\"Email\", Resources.EmailBanned);\n\t\tif (await _userService.IsIPBanned(ip))\n\t\t\tmodelState.AddModelError(\"Email\", Resources.IPBanned);\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\tpublic async Task<ViewResult> Verify(string id)\n\t{\n\t\tvar authKey = Guid.Empty;\n\t\tif (!string.IsNullOrWhiteSpace(id) && !Guid.TryParse(id, out authKey))\n\t\t\treturn View(\"VerifyFail\");\n\t\tif (string.IsNullOrWhiteSpace(id))\n\t\t\treturn View();\n\t\tvar user = await _userService.VerifyAuthorizationCode(authKey, HttpContext.Connection.RemoteIpAddress.ToString());\n\t\tif (user == null)\n\t\t\treturn View(\"VerifyFail\");\n\t\tViewData[\"Result\"] = Resources.AccountVerified;\n\t\tawait _userService.Login(user, HttpContext.Connection.RemoteIpAddress.ToString());\n\t\treturn View();\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic RedirectToActionResult VerifyCode(string authorizationCode)\n\t{\n\t\treturn RedirectToAction(\"Verify\", new { id = authorizationCode });\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\tpublic async Task<ViewResult> RequestCode(string email)\n\t{\n\t\tvar user = await _userService.GetUserByEmail(email);\n\t\tif (user == null)\n\t\t{\n\t\t\tViewData[\"Result\"] = Resources.NoUserFoundWithEmail;\n\t\t\treturn View(\"Verify\", new { id = String.Empty });\n\t\t}\n\t\tvar verifyUrl = Url.Action(\"Verify\", \"Account\", null, Request.Scheme);\n\t\tvar result = _newAccountMailer.Send(user, verifyUrl);\n\t\tif (result != SmtpStatusCode.Ok)\n\t\t\tViewData[\"EmailProblem\"] = Resources.EmailProblemAccount + result + \".\";\n\t\telse\n\t\t\tViewData[\"Result\"] = Resources.VerificationEmailSent;\n\t\treturn View(\"Verify\", new { id = String.Empty });\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic ViewResult Forgot()\n\t{\n\t\treturn View();\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic async Task<ViewResult> Forgot(string email)\n\t{\n\t\tvar user = await _userService.GetUserByEmail(email);\n\t\tif (user == null)\n\t\t{\n\t\t\tViewBag.Result = Resources.EmailNotFound;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tViewBag.Result = Resources.ForgotInstructionsSent;\n\t\t\tvar resetLink = Url.Action(\"ResetPassword\", \"Account\", null, Request.Scheme);\n\t\t\tawait _userService.GeneratePasswordResetEmail(user, resetLink);\n\t\t}\n\t\treturn View();\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\tpublic async Task<ActionResult> ResetPassword(string id)\n\t{\n\t\tvar authKey = Guid.Empty;\n\t\tif (!string.IsNullOrWhiteSpace(id) && !Guid.TryParse(id, out authKey))\n\t\t\treturn StatusCode(403);\n\t\tvar user = await _userService.GetUserByAuhtorizationKey(authKey);\n\t\tvar container = new PasswordResetContainer();\n\t\tif (user == null)\n\t\t\tcontainer.IsValidUser = false;\n\t\telse\n\t\t\tcontainer.IsValidUser = true;\n\t\treturn View(container);\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic async Task<ActionResult> ResetPassword(string id, PasswordResetContainer resetContainer)\n\t{\n\t\tvar authKey = Guid.Empty;\n\t\tif (!string.IsNullOrWhiteSpace(id) && !Guid.TryParse(id, out authKey))\n\t\t\treturn StatusCode(403);\n\t\tvar user = await _userService.GetUserByAuhtorizationKey(authKey);\n\t\tresetContainer.IsValidUser = true;\n\t\tif (resetContainer.Password != resetContainer.PasswordRetype)\n\t\t\tModelState.AddModelError(\"PasswordRetype\", Resources.RetypePasswordMustMatch);\n\t\tstring errorMessage;\n\t\t_userService.IsPasswordValid(resetContainer.Password, out errorMessage);\n\t\tif (!string.IsNullOrWhiteSpace(errorMessage))\n\t\t\tModelState.AddModelError(\"Password\", errorMessage);\n\t\tif (!ModelState.IsValid)\n\t\t\treturn View(resetContainer);\n\t\tawait _userService.ResetPassword(user, resetContainer.Password, HttpContext.Connection.RemoteIpAddress.ToString());\n\t\treturn RedirectToAction(\"ResetPasswordSuccess\");\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\tpublic ActionResult ResetPasswordSuccess()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn RedirectToAction(\"Login\");\n\t\treturn View();\n\t}\n\n\tpublic async Task<ViewResult> EditProfile()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tvar profile = await _profileService.GetProfileForEdit(user);\n\t\tvar userEdit = new UserEditProfile(profile);\n\t\treturn View(userEdit);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ViewResult> EditProfile(UserEditProfile userEdit)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tawait _profileService.EditUserProfile(user, userEdit);\n\t\tViewBag.Result = Resources.ProfileUpdated;\n\t\tvar profile = await _profileService.GetProfileForEdit(user);\n\t\tvar newEdit = new UserEditProfile(profile);\n\t\treturn View(newEdit);\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\tpublic ViewResult Security()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tvar isNewUserApproved = _settingsManager.Current.IsNewUserApproved;\n\t\tvar userEdit = new UserEditSecurity(user, isNewUserApproved);\n\t\treturn View(userEdit);\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic async Task<ViewResult> ChangePassword(UserEditSecurity userEdit)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tvar (isPasswordPassed, _) = await _userService.CheckPassword(user.Email, userEdit.OldPassword);\n\t\tif (!isPasswordPassed)\n\t\t\tViewBag.PasswordResult = Resources.OldPasswordIncorrect;\n\t\telse if (!userEdit.NewPasswordsMatch())\n\t\t\tViewBag.PasswordResult = Resources.RetypePasswordMustMatch;\n\t\telse if (!_userService.IsPasswordValid(userEdit.NewPassword, out var errorMessage))\n\t\t\tViewBag.PasswordResult = errorMessage;\n\t\telse\n\t\t{\n\t\t\tawait _userService.SetPassword(user, userEdit.NewPassword, HttpContext.Connection.RemoteIpAddress.ToString(), user);\n\t\t\tViewBag.PasswordResult = Resources.NewPasswordSaved;\n\t\t}\n\t\treturn View(\"Security\", new UserEditSecurity { NewEmail = String.Empty, NewEmailRetype = String.Empty, IsNewUserApproved = _settingsManager.Current.IsNewUserApproved });\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic async Task<ViewResult> ChangeEmail(UserEditSecurity userEdit)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tif (string.IsNullOrWhiteSpace(userEdit.NewEmail) || !userEdit.NewEmail.IsEmailAddress())\n\t\t\tViewBag.EmailResult = Resources.ValidEmailAddressRequired;\n\t\telse if (userEdit.NewEmail != userEdit.NewEmailRetype)\n\t\t\tViewBag.EmailResult = Resources.EmailsMustMatch;\n\t\telse if (await _userService.IsEmailInUseByDifferentUser(user, userEdit.NewEmail))\n\t\t\tViewBag.EmailResult = Resources.EmailInUse;\n\t\telse\n\t\t{\n\t\t\tawait _userService.ChangeEmail(user, userEdit.NewEmail, user, HttpContext.Connection.RemoteIpAddress.ToString());\n\t\t\tif (_settingsManager.Current.IsNewUserApproved)\n\t\t\t\tViewBag.EmailResult = Resources.EmailChangeSuccess;\n\t\t\telse\n\t\t\t{\n\t\t\t\tViewBag.EmailResult = Resources.VerificationEmailSent;\n\t\t\t\tvar verifyUrl = Url.Action(\"Verify\", \"Account\", null, Request.Scheme);\n\t\t\t\tvar result = _newAccountMailer.Send(user, verifyUrl);\n\t\t\t\tif (result != SmtpStatusCode.Ok)\n\t\t\t\t\tViewBag.EmailResult = Resources.EmailProblemAccount + result;\n\t\t\t}\n\t\t}\n\t\treturn View(\"Security\", new UserEditSecurity { NewEmail = String.Empty, NewEmailRetype = String.Empty, IsNewUserApproved = _settingsManager.Current.IsNewUserApproved });\n\t}\n\n\tpublic async Task<ViewResult> ManagePhotos()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tvar profile = await _profileService.GetProfile(user);\n\t\tvar userEdit = new UserEditPhoto(profile);\n\t\tif (profile.ImageID.HasValue)\n\t\t\tuserEdit.IsImageApproved = await _imageService.IsUserImageApproved(profile.ImageID.Value);\n\t\treturn View(userEdit);\n\t}\n\t\t\n\t[HttpPost]\n\tpublic async Task<ActionResult> ManagePhotos(UserEditPhoto userEdit)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tbyte[] avatarFile = null;\n\t\tif (userEdit.AvatarFile != null)\n\t\t\tavatarFile = userEdit.AvatarFile.OpenReadStream().ToBytes();\n\t\tbyte[] photoFile = null;\n\t\tif (userEdit.PhotoFile != null)\n\t\t\tphotoFile = userEdit.PhotoFile.OpenReadStream().ToBytes();\n\t\tawait _userService.EditUserProfileImages(user, userEdit.DeleteAvatar, userEdit.DeleteImage, avatarFile, photoFile);\n\t\treturn RedirectToAction(\"ManagePhotos\");\n\t}\n\n\tpublic async Task<ViewResult> MiniProfile(int id)\n\t{\n\t\tvar user = await _userService.GetUser(id);\n\t\tif (user == null)\n\t\t\treturn View(\"MiniUserNotFound\");\n\t\tvar profile = await _profileService.GetProfile(user);\n\t\tUserImage userImage = null;\n\t\tif (profile.ImageID.HasValue)\n\t\t\tuserImage = await _imageService.GetUserImage(profile.ImageID.Value);\n\t\tvar model = new DisplayProfile(user, profile, userImage);\n\t\tmodel.PostCount = await _postService.GetPostCount(user);\n\t\tvar viewingUser = _userRetrievalShim.GetUser();\n\t\tif (viewingUser == null)\n\t\t\tmodel.ShowDetails = false;\n\t\treturn View(model);\n\t}\n\n\tpublic async Task<ActionResult> ViewProfile(int id)\n\t{\n\t\tvar user = await _userService.GetUser(id);\n\t\tif (user == null)\n\t\t\treturn NotFound();\n\t\tvar profile = await _profileService.GetProfile(user);\n\t\tUserImage userImage = null;\n\t\tif (profile.ImageID.HasValue)\n\t\t\tuserImage = await _imageService.GetUserImage(profile.ImageID.Value);\n\t\tvar model = new DisplayProfile(user, profile, userImage);\n\t\tmodel.PostCount = await _postService.GetPostCount(user);\n\t\tmodel.Feed = await _feedService.GetFeed(user);\n\t\tmodel.UserAwards = await _userAwardService.GetAwards(user);\n\t\tvar viewingUser = _userRetrievalShim.GetUser();\n\t\tif (viewingUser == null)\n\t\t\tmodel.ShowDetails = false;\n\t\treturn View(model);\n\t}\n\n\tpublic async Task<ActionResult> Posts(int id, int pageNumber = 1)\n\t{\n\t\tvar postUser = await _userService.GetUser(id);\n\t\tif (postUser == null)\n\t\t\treturn NotFound();\n\t\tvar includeDeleted = false;\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\tincludeDeleted = true;\n\t\tvar titles = _forumService.GetAllForumTitles();\n\t\tvar (topics, pagerContext) = await _topicService.GetTopics(user, postUser, includeDeleted, pageNumber);\n\t\tvar container = new PagedTopicContainer { ForumTitles = titles, PagerContext = pagerContext, Topics = topics };\n\t\tawait _lastReadService.GetTopicReadStatus(user, container);\n\t\tViewBag.PostUserName = postUser.Name;\n\t\treturn View(container);\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic ActionResult Login()\n\t{\n\t\tif (_config.IsOAuthOnly)\n\t\t{\n\t\t\treturn Redirect(\"OAuthLogin\");\n\t\t}\n\t\t\n\t\tvar referer = Request.Headers.Referer.ToString();\n\t\tvar link = Url.IsLocalUrl(referer) ? referer : Url.Action(\"Index\", HomeController.Name);\n\t\tViewBag.Referrer = link;\n\n\t\tvar externalLoginList = _externalLoginRoutingService.GetActiveProviderTypeAndNameDictionary();\n\t\t\n\t\treturn View(externalLoginList);\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic IActionResult OAuthLogin()\n\t{\n\t\tif (_config.IsOAuthOnly)\n\t\t{\n\t\t\tvar identityProviderRedirectUrl = Url.Action(nameof(IdentityController.CallbackHandler), IdentityController.Name, null, Request.Scheme);\n\t\t\tvar redirect = _oAuthOnlyService.GetLoginUrl(identityProviderRedirectUrl);\n\t\t\tvar loginState = new ExternalLoginState {ProviderType = ProviderType.OAuthOnly, ReturnUrl = identityProviderRedirectUrl };\n\t\t\t_externalLoginTempService.Persist(loginState);\n\t\t\treturn View(\"OAuthLogin\", redirect);\n\t\t}\n\n\t\treturn RedirectToAction(\"Login\");\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic async Task<ViewResult> Unsubscribe(int id, string key)\n\t{\n\t\tvar user = await _userService.GetUser(id);\n\t\tif (user == null || (await _profileService.Unsubscribe(user, key) == false))\n\t\t\treturn View(\"UnsubscribeFailure\");\n\t\treturn View();\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\tpublic async Task<ViewResult> ExternalLogins()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tvar externalAssociations = await _externalUserAssociationManager.GetExternalUserAssociations(user);\n\t\tViewBag.Referrer = Url.Action(\"ExternalLogins\");\n\t\treturn View(externalAssociations);\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\tpublic async Task<ActionResult> RemoveExternalLogin(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(\"EditAccountNoUser\");\n\t\tawait _externalUserAssociationManager.RemoveAssociation(user, id, HttpContext.Connection.RemoteIpAddress.ToString());\n\t\treturn RedirectToAction(\"ExternalLogins\");\n\t}\n\n\tpublic RedirectToActionResult MyProfile()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn RedirectToAction(\"Create\");\n\t\treturn RedirectToAction(\"ViewProfile\", new {id = user.UserID});\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/AdminApiController.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Authorize(Policy = PermanentRoles.Admin, AuthenticationSchemes = PopForumsAuthenticationDefaults.AuthenticationScheme)]\n[Area(\"Forums\")]\n[Produces(\"application/json\")]\n[ApiController]\npublic class AdminApiController : Controller\n{\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly ICategoryService _categoryService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly IUserService _userService;\n\tprivate readonly ISearchService _searchService;\n\tprivate readonly IProfileService _profileService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly IImageService _imageService;\n\tprivate readonly IBanService _banService;\n\tprivate readonly IMailingListService _mailingListService;\n\tprivate readonly IEventDefinitionService _eventDefinitionService;\n\tprivate readonly IAwardDefinitionService _awardDefinitionService;\n\tprivate readonly IEventPublisher _eventPublisher;\n\tprivate readonly IIPHistoryService _ipHistoryService;\n\tprivate readonly ISecurityLogService _securityLogService;\n\tprivate readonly IModerationLogService _moderationLogService;\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly IServiceHeartbeatService _serviceHeartbeatService;\n\n\tpublic AdminApiController(ISettingsManager settingsManager, ICategoryService categoryService, IForumService forumService, IUserService userService, ISearchService searchService, IProfileService profileService, IUserRetrievalShim userRetrievalShim, IImageService imageService, IBanService banService, IMailingListService mailingListService, IEventDefinitionService eventDefinitionService, IAwardDefinitionService awardDefinitionService, IEventPublisher eventPublisher, IIPHistoryService ipHistoryService, ISecurityLogService securityLogService, IModerationLogService moderationLogService, IErrorLog errorLog, IServiceHeartbeatService serviceHeartbeatService)\n\t{\n\t\t_settingsManager = settingsManager;\n\t\t_categoryService = categoryService;\n\t\t_forumService = forumService;\n\t\t_userService = userService;\n\t\t_searchService = searchService;\n\t\t_profileService = profileService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_imageService = imageService;\n\t\t_banService = banService;\n\t\t_mailingListService = mailingListService;\n\t\t_eventDefinitionService = eventDefinitionService;\n\t\t_awardDefinitionService = awardDefinitionService;\n\t\t_eventPublisher = eventPublisher;\n\t\t_ipHistoryService = ipHistoryService;\n\t\t_securityLogService = securityLogService;\n\t\t_moderationLogService = moderationLogService;\n\t\t_errorLog = errorLog;\n\t\t_serviceHeartbeatService = serviceHeartbeatService;\n\t}\n\n\t// ********** settings\n\n\t[HttpGet(\"/Forums/AdminApi/GetSettings\")]\n\tpublic ActionResult<Settings> GetSettings()\n\t{\n\t\tvar settings = _settingsManager.Current;\n\t\treturn settings;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/SaveSettings\")]\n\tpublic ActionResult<Settings> SaveSettings([FromBody]Settings settings)\n\t{\n\t\t_settingsManager.Save(settings);\n\t\tvar newSettings = _settingsManager.Current;\n\t\treturn newSettings;\n\t}\n\n\t// ********** categories\n\n\t[HttpGet(\"/Forums/AdminApi/GetCategories\")]\n\tpublic async Task<ActionResult<List<Category>>> GetCategories()\n\t{\n\t\tvar categories = await _categoryService.GetAll();\n\t\treturn categories;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/AddCategory\")]\n\tpublic async Task<ActionResult<List<Category>>> AddCategory([FromBody]Category category)\n\t{\n\t\tawait _categoryService.Create(category.Title);\n\t\tvar categories = await _categoryService.GetAll();\n\t\treturn categories;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/DeleteCategory/{id}\")]\n\tpublic async Task<ActionResult<List<Category>>> DeleteCategory(int id)\n\t{\n\t\tawait _categoryService.Delete(id);\n\t\tvar categories = await _categoryService.GetAll();\n\t\treturn categories;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/MoveCategoryUp/{id}\")]\n\tpublic async Task<ActionResult<List<Category>>> MoveCategoryUp(int id)\n\t{\n\t\tawait _categoryService.MoveCategoryUp(id);\n\t\tvar categories = await _categoryService.GetAll();\n\t\treturn categories;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/MoveCategoryDown/{id}\")]\n\tpublic async Task<ActionResult<List<Category>>> MoveCategoryDown(int id)\n\t{\n\t\tawait _categoryService.MoveCategoryDown(id);\n\t\tvar categories = await _categoryService.GetAll();\n\t\treturn categories;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/EditCategory\")]\n\tpublic async Task<ActionResult<List<Category>>> EditCategory([FromBody]Category category)\n\t{\n\t\tawait _categoryService.UpdateTitle(category.CategoryID, category.Title);\n\t\tvar categories = await _categoryService.GetAll();\n\t\treturn categories;\n\t}\n\n\t// ********** forums\n\n\t[HttpGet(\"/Forums/AdminApi/GetForums\")]\n\tpublic async Task<ActionResult<List<CategoryContainerWithForums>>> GetForums()\n\t{\n\t\tvar forums = await _forumService.GetCategoryContainersWithForums();\n\t\treturn forums;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/MoveForumUp/{id}\")]\n\tpublic async Task<ActionResult<List<CategoryContainerWithForums>>> MoveForumUp(int id)\n\t{\n\t\tawait _forumService.MoveForumUp(id);\n\t\tvar forums = await _forumService.GetCategoryContainersWithForums();\n\t\treturn forums;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/MoveForumDown/{id}\")]\n\tpublic async Task<ActionResult<List<CategoryContainerWithForums>>> MoveForumDown(int id)\n\t{\n\t\tawait _forumService.MoveForumDown(id);\n\t\tvar forums = await _forumService.GetCategoryContainersWithForums();\n\t\treturn forums;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/SaveForum\")]\n\tpublic async Task<ActionResult<List<CategoryContainerWithForums>>> SaveForum([FromBody]Forum forumEdit)\n\t{\n\t\tif (forumEdit.CategoryID == 0)\n\t\t\tforumEdit.CategoryID = null;\n\t\tif (forumEdit.ForumID == 0)\n\t\t\tawait _forumService.Create(forumEdit.CategoryID, forumEdit.Title, forumEdit.Description, forumEdit.IsVisible, forumEdit.IsArchived, -1, forumEdit.ForumAdapterName, forumEdit.IsQAForum);\n\t\telse\n\t\t{\n\t\t\tvar forum = await _forumService.Get(forumEdit.ForumID);\n\t\t\tif (forum == null)\n\t\t\t\treturn NotFound();\n\t\t\tawait _forumService.Update(forum, forumEdit.CategoryID, forumEdit.Title, forumEdit.Description, forumEdit.IsVisible, forumEdit.IsArchived, forumEdit.ForumAdapterName, forumEdit.IsQAForum);\n\t\t}\n\t\tvar forums = await _forumService.GetCategoryContainersWithForums();\n\t\treturn forums;\n\t}\n\n\t// ********** forum permissions\n\n\t[HttpGet(\"/Forums/AdminApi/GetForumPermissions/{id}\")]\n\tpublic async Task<ActionResult<ForumPermissionContainer>> GetForumPermissions(int id)\n\t{\n\t\tvar forum = await _forumService.Get(id);\n\t\tif (forum == null)\n\t\t\treturn NotFound();\n\t\tvar container = new ForumPermissionContainer\n\t\t{\n\t\t\tForumID = forum.ForumID,\n\t\t\tAllRoles = await _userService.GetAllRoles(),\n\t\t\tPostRoles = await _forumService.GetForumPostRoles(forum),\n\t\t\tViewRoles = await _forumService.GetForumViewRoles(forum)\n\t\t};\n\t\treturn container;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/ModifyForumRoles\")]\n\tpublic async Task<NoContentResult> ModifyForumRoles(ModifyForumRolesContainer container)\n\t{\n\t\tawait _forumService.ModifyForumRoles(container);\n\t\treturn NoContent();\n\t}\n\n\t// ********** search\n\n\t[HttpGet(\"/Forums/AdminApi/GetJunkWords\")]\n\tpublic async Task<ActionResult<IEnumerable<string>>> GetJunkWords()\n\t{\n\t\tvar words = await _searchService.GetJunkWords();\n\t\treturn words;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/CreateJunkWord/{word}\")]\n\tpublic async Task<NoContentResult> CreateJunkWord(string word)\n\t{\n\t\tawait _searchService.CreateJunkWord(word);\n\t\treturn NoContent();\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/DeleteJunkWord/{word}\")]\n\tpublic async Task<NoContentResult> DeleteJunkWord(string word)\n\t{\n\t\tawait _searchService.DeleteJunkWord(word);\n\t\treturn NoContent();\n\t}\n\n\t// ********** recent users\n\n\t[HttpGet(\"/Forums/AdminApi/GetRecentUsers\")]\n\tpublic async Task<ActionResult<List<UserResult>>> GetRecentUsers()\n\t{\n\t\tvar userResults = await _userService.GetRecentUsers();\n\t\treturn userResults;\n\t}\n\n\t// ********** edit user\n\n\t[HttpPost(\"/Forums/AdminApi/EditUserSearch\")]\n\tpublic async Task<ActionResult<List<User>>> EditUserSearch(UserSearch userSearch)\n\t{\n\t\tList<User> users;\n\t\tswitch (userSearch.SearchType)\n\t\t{\n\t\t\tcase UserSearch.UserSearchType.Email:\n\t\t\t\tusers = await _userService.SearchByEmail(userSearch.SearchText);\n\t\t\t\tbreak;\n\t\t\tcase UserSearch.UserSearchType.Name:\n\t\t\t\tusers = await _userService.SearchByName(userSearch.SearchText);\n\t\t\t\tbreak;\n\t\t\tcase UserSearch.UserSearchType.Role:\n\t\t\t\tusers = await _userService.SearchByRole(userSearch.SearchText);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new ArgumentOutOfRangeException(nameof(userSearch));\n\t\t}\n\t\treturn users;\n\t}\n\n\t[HttpGet(\"/Forums/AdminApi/GetUser/{id}\")]\n\tpublic async Task<ActionResult<UserEdit>> GetUser(int id)\n\t{\n\t\tvar user = await _userService.GetUser(id);\n\t\tif (user == null)\n\t\t\treturn NotFound();\n\t\tvar profile = await _profileService.GetProfileForEdit(user, true);\n\t\tvar model = new UserEdit(user, profile);\n\t\treturn model;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/UpdateUserAvatar/{id}\")]\n\tpublic async Task<ActionResult<dynamic>> UpdateUserAvatar(int id)\n\t{\n\t\tvar user = await _userService.GetUser(id);\n\t\tif (user == null)\n\t\t\treturn NotFound();\n\t\tif (Request.Form?.Files?.Count != 1)\n\t\t{\n\t\t\tawait _userService.EditUserProfileImages(user, true, false, null, null);\n\t\t\treturn new {AvatarID = (int?)null};\n\t\t}\n\t\tvar file = Request.Form.Files[0];\n\t\tawait _userService.EditUserProfileImages(user, false, false, file.OpenReadStream().ToBytes(), null);\n\t\tvar profile = await _profileService.GetProfileForEdit(user, true);\n\t\treturn new {profile.AvatarID};\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/UpdateUserImage/{id}\")]\n\tpublic async Task<ActionResult<dynamic>> UpdateUserImage(int id)\n\t{\n\t\tvar user = await _userService.GetUser(id);\n\t\tif (user == null)\n\t\t\treturn NotFound();\n\t\tif (Request.Form?.Files?.Count != 1)\n\t\t{\n\t\t\tawait _userService.EditUserProfileImages(user, false, true, null, null);\n\t\t\treturn new { ImageID = (int?)null };\n\t\t}\n\t\tvar file = Request.Form.Files[0];\n\t\tawait _userService.EditUserProfileImages(user, false, false, null, file.OpenReadStream().ToBytes());\n\t\tvar profile = await _profileService.GetProfile(user);\n\t\treturn new { profile.ImageID };\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/SaveUser\")]\n\tpublic async Task<ActionResult> SaveUser([FromBody] UserEdit userEdit)\n\t{\n\t\tvar adminUser = _userRetrievalShim.GetUser();\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress.ToString();\n\t\tvar user = await _userService.GetUser(userEdit.UserID);\n\t\tawait _userService.EditUser(user, userEdit, false, false, null, null, ip, adminUser);\n\t\treturn Ok();\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/DeleteUser/{id}\")]\n\tpublic async Task<ActionResult> DeleteUser(int id)\n\t{\n\t\tawait DeleteUser(id, false);\n\t\treturn Ok();\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/DeleteAndBanUser/{id}\")]\n\tpublic async Task<ActionResult> DeleteAndBanUser(int id)\n\t{\n\t\tawait DeleteUser(id, true);\n\t\treturn Ok();\n\t}\n\n\tprivate async Task DeleteUser(int userID, bool isBanned)\n\t{\n\t\tvar adminUser = _userRetrievalShim.GetUser();\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress.ToString();\n\t\tvar user = await _userService.GetUser(userID);\n\t\tawait _userService.DeleteUser(user, adminUser, ip, isBanned);\n\t}\n\n\t// ********** user roles\n\n\t[HttpGet(\"/Forums/AdminApi/GetAllRoles\")]\n\tpublic async Task<ActionResult<IEnumerable<string>>> GetAllRoles()\n\t{\n\t\tvar roles = await _userService.GetAllRoles();\n\t\treturn roles;\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/CreateRole/{role}\")]\n\tpublic async Task<ActionResult> CreateRole(string role)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress.ToString();\n\t\tawait _userService.CreateRole(role, user, ip);\n\t\treturn NoContent();\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/DeleteRole/{role}\")]\n\tpublic async Task<ActionResult> DeleteRole(string role)\n\t{\n\t\tif (role == PermanentRoles.Admin || role == PermanentRoles.Moderator)\n\t\t\treturn NoContent();\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress.ToString();\n\t\tawait _userService.DeleteRole(role, user, ip);\n\t\treturn NoContent();\n\t}\n\n\t// ********** user image approval\n\n\t[HttpGet(\"/Forums/AdminApi/GetImageApproval\")]\n\tpublic async Task<ActionResult<UserImageApprovalContainer>> GetImageApproval()\n\t{\n\t\tvar container = await _imageService.GetUnapprovedUserImageContainer();\n\t\treturn container;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/ApproveUserImage/{id}\")]\n\tpublic async Task<ActionResult> ApproveUserImage(int id)\n\t{\n\t\tawait _imageService.ApproveUserImage(id);\n\t\treturn NoContent();\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/DeleteUserImage/{id}\")]\n\tpublic async Task<ActionResult> DeleteUserImage(int id)\n\t{\n\t\tawait _imageService.DeleteUserImage(id);\n\t\treturn NoContent();\n\t}\n\n\t// ********** email ip ban\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpGet(\"/Forums/AdminApi/GetEmailIPBan\")]\n\tpublic async Task<ActionResult<object>> GetEmailIPBan()\n\t{\n\t\tvar emails = await _banService.GetEmailBans();\n\t\tvar ips = await _banService.GetIPBans();\n\t\tvar container = new {emails, ips};\n\t\treturn container;\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/BanEmail\")]\n\tpublic async Task<ActionResult> BanEmail([FromBody] SingleString val)\n\t{\n\t\tawait _banService.BanEmail(val.String);\n\t\treturn NoContent();\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/RemoveEmail\")]\n\tpublic async Task<ActionResult> RemoveEmail([FromBody] SingleString val)\n\t{\n\t\tawait _banService.RemoveEmailBan(val.String);\n\t\treturn NoContent();\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/BanIP\")]\n\tpublic async Task<ActionResult> BanIP([FromBody] SingleString val)\n\t{\n\t\tawait _banService.BanIP(val.String);\n\t\treturn NoContent();\n\t}\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/RemoveIP\")]\n\tpublic async Task<ActionResult> RemoveIP([FromBody] SingleString val)\n\t{\n\t\tawait _banService.RemoveIPBan(val.String);\n\t\treturn NoContent();\n\t}\n\n\t// ********** email users\n\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost(\"/Forums/AdminApi/EmailUsers\")]\n\tpublic ActionResult EmailUsers([FromBody]EmailUsersContainer container)\n\t{\n\t\tif (string.IsNullOrWhiteSpace(container.Subject) || string.IsNullOrWhiteSpace(container.Body))\n\t\t\treturn StatusCode((int)HttpStatusCode.BadRequest, new {Error = Resources.SubjectAndBodyNotEmpty});\n\t\tvar baseString = Url.Action(\"Unsubscribe\", AccountController.Name, new { id = \"--id--\", key = \"--key--\" }, Request.Scheme);\n\t\tbaseString = baseString.Replace(\"--id--\", \"{0}\").Replace(\"--key--\", \"{1}\");\n\t\tstring UnsubscribeLinkGenerator(User user) => string.Format(baseString, user.UserID, _profileService.GetUnsubscribeHash(user));\n\t\t_mailingListService.MailUsers(container.Subject, container.Body, container.HtmlBody, UnsubscribeLinkGenerator);\n\t\treturn Ok();\n\t}\n\n\t// ********** event definitions\n\n\t[HttpGet(\"/Forums/AdminApi/GetAllEventDefinitions\")]\n\tpublic async Task<ActionResult<object>> GetAllEventDefinitions()\n\t{\n\t\tvar events = await _eventDefinitionService.GetAll();\n\t\tvar staticIDs = EventDefinitionService.StaticEvents.Select(x => x.Key).ToArray();\n\t\tvar container = new {AllEvents = events, StaticIDs = staticIDs};\n\t\treturn container;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/CreateEvent\")]\n\tpublic async Task<ActionResult> CreateEvent([FromBody]EventDefinition newEvent)\n\t{\n\t\tawait _eventDefinitionService.Create(newEvent);\n\t\treturn Ok();\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/DeleteEvent/{id}\")]\n\tpublic async Task<ActionResult> DeleteEvent(string id)\n\t{\n\t\tawait _eventDefinitionService.Delete(id);\n\t\treturn Ok();\n\t}\n\n\t// ********** award definitions\n\n\t[HttpGet(\"/Forums/AdminApi/GetAllAwardDefinitions\")]\n\tpublic async Task<ActionResult<List<AwardDefinition>>> GetAllAwardDefinitions()\n\t{\n\t\tvar awardDefinitions = await _awardDefinitionService.GetAll();\n\t\treturn awardDefinitions;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/CreateAward\")]\n\tpublic async Task<ActionResult> CreateAward([FromBody]AwardDefinition newAward)\n\t{\n\t\tawait _awardDefinitionService.Create(newAward);\n\t\treturn Ok();\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/DeleteAward/{id}\")]\n\tpublic async Task<ActionResult> DeleteAward(string id)\n\t{\n\t\tawait _awardDefinitionService.Delete(id);\n\t\treturn Ok();\n\t}\n\n\t[HttpGet(\"/Forums/AdminApi/GetAward/{id}\")]\n\tpublic async Task<ActionResult<object>> GetAward(string id)\n\t{\n\t\tvar award = await _awardDefinitionService.Get(id);\n\t\tvar conditions = await _awardDefinitionService.GetConditions(award.AwardDefinitionID);\n\t\tvar allEvents = await _eventDefinitionService.GetAll();\n\t\tvar container = new {Award = award, Conditions = conditions, AllEvents = allEvents};\n\t\treturn container;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/CreateCondition\")]\n\tpublic async Task<ActionResult> CreateCondition([FromBody]AwardCondition newCondition)\n\t{\n\t\tawait _awardDefinitionService.AddCondition(newCondition);\n\t\treturn Ok();\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/DeleteCondition\")]\n\tpublic async Task<ActionResult> DeleteCondition([FromBody]AwardConditionDeleteContainer container)\n\t{\n\t\tawait _awardDefinitionService.DeleteCondition(container.AwardDefinitionID, container.EventDefinitionID);\n\t\treturn Ok();\n\t}\n\n\t// ********** manual event\n\n\t[HttpPost(\"/Forums/AdminApi/GetNames\")]\n\tpublic async Task<ActionResult<IEnumerable<object>>> GetNames(SingleString name)\n\t{\n\t\tvar users = await _userService.SearchByName(name.String);\n\t\tvar projection = users.Select(u => new { u.UserID, u.Name }).ToArray();\n\t\treturn projection;\n\t}\n\n\t[HttpGet(\"/Forums/AdminApi/GetAllEvents\")]\n\tpublic async Task<ActionResult<IEnumerable<EventDefinition>>> GetAllEvents()\n\t{\n\t\tvar events = await _eventDefinitionService.GetAll();\n\t\treturn events;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/CreateManualEvent\")]\n\tpublic async Task<ActionResult> CreateManualEvent([FromBody] ManualEvent manualEvent)\n\t{\n\t\tif (!string.IsNullOrEmpty(manualEvent.EventDefinitionID))\n\t\t\treturn BadRequest(\"Can't specify an EventDefinitionID.\");\n\t\tvar user = await _userService.GetUser(manualEvent.UserID);\n\t\tif (user == null)\n\t\t\treturn BadRequest($\"UserID {manualEvent.UserID} does not exist.\");\n\t\tif (!manualEvent.Points.HasValue)\n\t\t\treturn BadRequest(\"Point value required.\");\n\t\tawait _eventPublisher.ProcessManualEvent(manualEvent.Message, user, manualEvent.Points.Value);\n\t\treturn Ok();\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/CreateExistingManualEvent\")]\n\tpublic async Task<ActionResult> CreateExistingManualEvent([FromBody] ManualEvent manualEvent)\n\t{\n\t\tif (string.IsNullOrEmpty(manualEvent.EventDefinitionID))\n\t\t\treturn BadRequest(\"Must specify an EventDefinitionID.\");\n\t\tvar user = await _userService.GetUser(manualEvent.UserID);\n\t\tif (user == null)\n\t\t\treturn BadRequest($\"UserID {manualEvent.UserID} does not exist.\");\n\t\tif (manualEvent.Points.HasValue)\n\t\t\treturn BadRequest(\"Point value can't be specified.\");\n\t\tawait _eventPublisher.ProcessEvent(manualEvent.Message, user, manualEvent.EventDefinitionID, false);\n\t\treturn Ok();\n\t}\n\n\t// ********** ip history\n\n\t[HttpPost(\"/Forums/AdminApi/QueryIPHistory\")]\n\tpublic async Task<ActionResult<List<IPHistoryEvent>>> QueryIPHistory([FromBody] IPHistoryQuery query)\n\t{\n\t\tvar history = await _ipHistoryService.GetHistory(query.IP, query.Start, query.End);\n\t\treturn history;\n\t}\n\n\t// ********** security log\n\n\t[HttpPost(\"/Forums/AdminApi/QuerySecurityLog\")]\n\tpublic async Task<ActionResult<List<SecurityLogEntry>>> QuerySecurityLog([FromBody] SecurityLogQuery query)\n\t{\n\t\tList<SecurityLogEntry> list;\n\t\tswitch (query.Type.ToLower())\n\t\t{\n\t\t\tcase \"userid\":\n\t\t\t\tlist = await _securityLogService.GetLogEntriesByUserID(Convert.ToInt32(query.SearchTerm), query.Start, query.End);\n\t\t\t\tbreak;\n\t\t\tcase \"name\":\n\t\t\t\tlist = await _securityLogService.GetLogEntriesByUserName(query.SearchTerm, query.Start, query.End);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\treturn BadRequest(\"Search type invalid.\");\n\t\t}\n\t\treturn list;\n\t}\n\n\t// ********** moderation log\n\n\t[HttpPost(\"/Forums/AdminApi/QueryModerationLog\")]\n\tpublic async Task<ActionResult<List<ModerationLogEntry>>> QueryModerationLog([FromBody] IPHistoryQuery query)\n\t{\n\t\tvar history = await _moderationLogService.GetLog(query.Start, query.End);\n\t\treturn history;\n\t}\n\n\t// ********** error log\n\n\t[HttpGet(\"/Forums/AdminApi/GetErrorLog/{pageNumber}\")]\n\tpublic ActionResult<PagedList<ErrorLogEntry>> GetErrorLog(int pageNumber)\n\t{\n\t\tvar list = _errorLog.GetErrors(pageNumber, 20);\n\t\treturn list;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/DeleteAllErrors\")]\n\tpublic async Task<ActionResult> DeleteAllErrors()\n\t{\n\t\tawait _errorLog.DeleteAllErrors();\n\t\treturn Ok();\n\t}\n\n\t// ********** error log\n\n\t[HttpGet(\"/Forums/AdminApi/GetServices\")]\n\tpublic async Task<ActionResult<List<ServiceHeartbeat>>> GetServices()\n\t{\n\t\tvar list = await _serviceHeartbeatService.GetAll();\n\t\treturn list;\n\t}\n\n\t[HttpPost(\"/Forums/AdminApi/ClearServices\")]\n\tpublic async Task<ActionResult<List<ServiceHeartbeat>>> ClearServices()\n\t{\n\t\tawait _serviceHeartbeatService.ClearAll();\n\t\tvar list = await _serviceHeartbeatService.GetAll();\n\t\treturn list;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/AdminController.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Authorize(Policy = PermanentRoles.Admin, AuthenticationSchemes = PopForumsAuthenticationDefaults.AuthenticationScheme)]\n[Area(\"Forums\")]\npublic class AdminController : Controller\n{\n\tpublic static string Name = \"Admin\";\n\n\tpublic ViewResult App(string vue = \"\")\n\t{\n\t\treturn View();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/ApiController.cs",
    "content": "﻿using PopForums.Messaging.Models;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[ApiController]\npublic class ApiController : Controller\n{\n\tprivate readonly INotificationAdapter _notificationAdapter;\n\tprivate readonly IConfig _config;\n\n\tpublic ApiController(INotificationAdapter notificationAdapter, IConfig config)\n\t{\n\t\t_notificationAdapter = notificationAdapter;\n\t\t_config = config;\n\t}\n\t\n\t[HttpPost(\"/Forums/Api/NotifyAward\")]\n\tpublic async Task<IActionResult> NotifyAward(AwardPayload awardPayload)\n\t{\n\t\tvar hash = _config.QueueConnectionString.GetSHA256Hash();\n\t\tvar result = HttpContext.Request.Headers.TryGetValue(NotificationTunnel.HeaderName, out var headerValue);\n\t\tif (headerValue != hash)\n\t\t\treturn Unauthorized();\n\t\tif (awardPayload == null)\n\t\t\treturn BadRequest();\n\t\tawait _notificationAdapter.Award(awardPayload.Title, awardPayload.UserID, awardPayload.TenantID);\n\t\treturn Ok();\n\t}\n\n\t[HttpPost(\"/Forums/Api/NotifyReply\")]\n\tpublic async Task<IActionResult> NotifyReply(ReplyPayload replyPayload)\n\t{\n\t\tvar hash = _config.QueueConnectionString.GetSHA256Hash();\n\t\tvar result = HttpContext.Request.Headers.TryGetValue(NotificationTunnel.HeaderName, out var headerValue);\n\t\tif (headerValue != hash)\n\t\t\treturn Unauthorized();\n\t\tif (replyPayload == null)\n\t\t\treturn BadRequest();\n\t\tawait _notificationAdapter.Reply(replyPayload.PostName, replyPayload.Title, replyPayload.TopicID, replyPayload.UserID, replyPayload.TenantID);\n\t\treturn Ok();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/FavoritesController.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class FavoritesController : Controller\n{\n\tpublic FavoritesController(IFavoriteTopicService favoriteTopicService, IForumService forumService, ILastReadService lastReadService, ITopicService topicService, IUserRetrievalShim userRetrievalShim)\n\t{\n\t\t_favoriteTopicService = favoriteTopicService;\n\t\t_forumService = forumService;\n\t\t_lastReadService = lastReadService;\n\t\t_topicService = topicService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t}\n\n\tprivate readonly IFavoriteTopicService _favoriteTopicService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly ILastReadService _lastReadService;\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\n\tpublic static string Name = \"Favorites\";\n\n\tpublic async Task<ViewResult> Topics(int pageNumber = 1)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View();\n\t\tvar (topics, pagerContext) = await _favoriteTopicService.GetTopics(user, pageNumber);\n\t\tvar titles = _forumService.GetAllForumTitles();\n\t\tvar container = new PagedTopicContainer { PagerContext = pagerContext, Topics = topics, ForumTitles = titles };\n\t\tawait _lastReadService.GetTopicReadStatus(user, container);\n\t\treturn View(container);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> RemoveFavorite(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar topic = await _topicService.Get(id);\n\t\tawait _favoriteTopicService.RemoveFavoriteTopic(user, topic);\n\t\treturn RedirectToAction(\"Topics\");\n\t}\n\n\t[HttpPost]\n\tpublic async Task<JsonResult> ToggleFavorite(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Json(new BasicJsonMessage { Message = Resources.NotLoggedIn, Result = false });\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\treturn Json(new BasicJsonMessage { Message = Resources.TopicNotExist, Result = false });\n\t\tif (await _favoriteTopicService.IsTopicFavorite(user.UserID, topic.TopicID))\n\t\t{\n\t\t\tawait _favoriteTopicService.RemoveFavoriteTopic(user, topic);\n\t\t\treturn Json(new BasicJsonMessage { Data = new { isFavorite = false }, Result = true });\n\t\t}\n\t\tawait _favoriteTopicService.AddFavoriteTopic(user, topic);\n\t\treturn Json(new BasicJsonMessage { Data = new { isFavorite = true }, Result = true });\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/ForumController.cs",
    "content": "﻿using PopForums.Composers;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class ForumController : Controller\n{\n\tpublic ForumController(ISettingsManager settingsManager, IForumService forumService, ITopicService topicService, IPostService postService, ITopicViewCountService topicViewCountService, ILastReadService lastReadService, IProfileService profileService, IUserRetrievalShim userRetrievalShim, ITopicViewLogService topicViewLogService, IPostMasterService postMasterService, IForumPermissionService forumPermissionService, ITopicStateComposer topicStateComposer, IForumStateComposer forumStateComposer, IIgnoreService ignoreService)\n\t{\n\t\t_settingsManager = settingsManager;\n\t\t_forumService = forumService;\n\t\t_topicService = topicService;\n\t\t_postService = postService;\n\t\t_topicViewCountService = topicViewCountService;\n\t\t_lastReadService = lastReadService;\n\t\t_profileService = profileService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_topicViewLogService = topicViewLogService;\n\t\t_postMasterService = postMasterService;\n\t\t_forumPermissionService = forumPermissionService;\n\t\t_topicStateComposer = topicStateComposer;\n\t\t_forumStateComposer = forumStateComposer;\n\t\t_ignoreService = ignoreService;\n\t}\n\n\tpublic static string Name = \"Forum\";\n\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly IPostService _postService;\n\tprivate readonly ITopicViewCountService _topicViewCountService;\n\tprivate readonly ILastReadService _lastReadService;\n\tprivate readonly IProfileService _profileService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly ITopicViewLogService _topicViewLogService;\n\tprivate readonly IPostMasterService _postMasterService;\n\tprivate readonly IForumPermissionService _forumPermissionService;\n\tprivate readonly ITopicStateComposer _topicStateComposer;\n\tprivate readonly IForumStateComposer _forumStateComposer;\n\tprivate readonly IIgnoreService _ignoreService;\n\n\tpublic async Task<ActionResult> Index(string urlName, int pageNumber = 1)\n\t{\n\t\tif (string.IsNullOrWhiteSpace(urlName))\n\t\t\treturn NotFound();\n\t\tvar forum = await _forumService.Get(urlName);\n\t\tif (forum == null)\n\t\t\treturn NotFound();\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user);\n\t\tif (!permissionContext.UserCanView)\n\t\t{\n\t\t\treturn StatusCode(403);\n\t\t}\n\n\t\tvar (topics, pagerContext) = await _topicService.GetTopics(forum, permissionContext.UserCanModerate, pageNumber);\n\t\tvar container = new ForumTopicContainer { Forum = forum, Topics = topics, PagerContext = pagerContext, PermissionContext = permissionContext };\n\t\tawait _lastReadService.GetTopicReadStatus(user, container);\n\t\tvar forumState = _forumStateComposer.GetState(forum, pagerContext);\n\t\tcontainer.ForumState = forumState;\n\t\tvar adapter = new ForumAdapterFactory(forum);\n\t\tif (adapter.IsAdapterEnabled)\n\t\t{\n\t\t\tawait adapter.ForumAdapter.AdaptForum(this, container);\n\t\t\tif (string.IsNullOrWhiteSpace(adapter.ForumAdapter.ViewName))\n\t\t\t\treturn View(adapter.ForumAdapter.Model);\n\t\t\treturn View(adapter.ForumAdapter.ViewName, adapter.ForumAdapter.Model);\n\t\t}\n\t\tif (forum.IsQAForum)\n\t\t\treturn View(\"IndexQA\", container);\n\t\treturn View(container);\n\t}\n\n\tpublic async Task<ActionResult> PostTopic(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Content(Resources.LoginToPost);\n\t\tvar (forum, permissionContext) = await GetForumByIdWithPermissionContext(id, user);\n\t\tif (!permissionContext.UserCanView)\n\t\t\treturn Content(Resources.ForumNoView);\n\t\tif (!permissionContext.UserCanPost)\n\t\t\treturn Content(Resources.ForumNoPost);\n\n\t\tvar profile = await _profileService.GetProfile(user);\n\t\tvar newPost = new NewPost { ItemID = forum.ForumID, IncludeSignature = profile.Signature.Length > 0, IsPlainText = profile.IsPlainText, IsImageEnabled = _settingsManager.Current.AllowImages };\n\t\treturn View(\"NewTopic\", newPost);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<IActionResult> PostTopic([FromBody]NewPost newPost)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Forbid();\n\t\tvar userProfileUrl = Url.Action(\"ViewProfile\", \"Account\", new { id = user.UserID });\n\t\tstring TopicLinkGenerator(Topic t) => Url.Action(\"Topic\", \"Forum\", new {id = t.UrlName});\n\t\tstring RedirectLinkGenerator(Topic t) => Url.RouteUrl(new {controller = \"Forum\", action = \"Topic\", id = t.UrlName});\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress.ToString();\n\n\t\tvar result = await _postMasterService.PostNewTopic(user, newPost, ip, userProfileUrl, TopicLinkGenerator, RedirectLinkGenerator);\n\n\t\tif (result.IsSuccessful)\n\t\t\treturn Json(new BasicJsonMessage {Result = true, Redirect = result.Redirect});\n\t\treturn Json(new BasicJsonMessage {Result = false, Message = result.Message});\n\t}\n\n\tprivate async Task<Tuple<Forum, ForumPermissionContext>> GetForumByIdWithPermissionContext(int forumID, User user)\n\t{\n\t\tvar forum = await _forumService.Get(forumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"Forum {forumID} not found\");\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user);\n\t\treturn Tuple.Create(forum, permissionContext);\n\t}\n\n\tprivate async Task<Tuple<ForumPermissionContext, Topic>> GetPermissionContextByTopicID(int topicID)\n\t{\n\t\tvar topic = await _topicService.Get(topicID);\n\t\tif (topic == null)\n\t\t\tthrow new Exception($\"Topic {topicID} not found\");\n\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"Forum {topic.ForumID} not found\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user);\n\t\treturn Tuple.Create(permissionContext, topic);\n\t}\n\n\tpublic async Task<ActionResult> TopicID(int id)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\treturn NotFound();\n\t\treturn RedirectToActionPermanent(\"Topic\", new { id = topic.UrlName });\n\t}\n\n\tpublic async Task<ActionResult> Topic(string id, int pageNumber = 1)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\treturn NotFound();\n\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"TopicID {topic.TopicID} references ForumID {topic.ForumID}, which does not exist.\");\n\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar adapter = new ForumAdapterFactory(forum);\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user, topic);\n\t\tif (!permissionContext.UserCanView)\n\t\t{\n\t\t\treturn NotFound();\n\t\t}\n\n\t\tPagerContext pagerContext = null;\n\t\tDateTime? lastReadTime = DateTime.UtcNow;\n\t\tif (user != null)\n\t\t{\n\t\t\tlastReadTime = await _lastReadService.GetLastReadTime(user, topic);\n\t\t\tif (!adapter.IsAdapterEnabled || (adapter.IsAdapterEnabled && adapter.ForumAdapter.MarkViewedTopicRead))\n\t\t\t\tawait _lastReadService.MarkTopicRead(user, topic);\n\t\t\tif (user.IsInRole(PermanentRoles.Moderator))\n\t\t\t{\n\t\t\t\tvar categorizedForums = await _forumService.GetCategorizedForumContainer();\n\t\t\t\tvar categorizedForumSelectList = new List<SelectListItem>();\n\t\t\t\tforeach (var uncategorizedForum in categorizedForums.UncategorizedForums)\n\t\t\t\t\tcategorizedForumSelectList.Add(new SelectListItem { Value = uncategorizedForum.ForumID.ToString(), Text = uncategorizedForum.Title, Selected = forum.ForumID == uncategorizedForum.ForumID});\n\t\t\t\tforeach (var categoryPair in categorizedForums.CategoryDictionary)\n\t\t\t\t{\n\t\t\t\t\tvar group = new SelectListGroup {Name = categoryPair.Key.Title};\n\t\t\t\t\tforeach (var categorizedForum in categoryPair.Value)\n\t\t\t\t\t\tcategorizedForumSelectList.Add(new SelectListItem { Value = categorizedForum.ForumID.ToString(), Text = categorizedForum.Title, Selected = forum.ForumID == categorizedForum.ForumID, Group = group});\n\t\t\t\t}\n\t\t\t\tViewBag.CategorizedForums = categorizedForumSelectList;\n\t\t\t}\n\t\t}\n\t\tList<Post> posts;\n\t\tif (forum.IsQAForum)\n\t\t\tposts = await _postService.GetPosts(topic, permissionContext.UserCanModerate);\n\t\telse\n\t\t\t(posts, pagerContext) = await _postService.GetPosts(topic, permissionContext.UserCanModerate, pageNumber);\n\t\tif (posts.Count == 0)\n\t\t\treturn NotFound();\n\t\tvar signatures = await _profileService.GetSignatures(posts);\n\t\tvar avatars = await _profileService.GetAvatars(posts);\n\t\tvar votedIDs = await _postService.GetVotedPostIDs(user, posts);\n\t\tvar ignores = await _ignoreService.GetIgnoredUserIdsInList(user, posts);\n\t\tvar container = ComposeTopicContainer(topic, forum, permissionContext, posts, pagerContext, signatures, avatars, votedIDs, lastReadTime, ignores);\n\t\tawait _topicViewCountService.ProcessView(topic);\n\t\tawait _topicViewLogService.LogView(user?.UserID, topic.TopicID);\n\t\tvar topicState = await _topicStateComposer.GetState(topic, pagerContext?.PageIndex, pagerContext?.PageCount, posts.Last().PostID);\n\t\tcontainer.TopicState = topicState;\n\t\tif (adapter.IsAdapterEnabled)\n\t\t{\n\t\t\tawait adapter.ForumAdapter.AdaptTopic(this, container);\n\t\t\tif (string.IsNullOrWhiteSpace(adapter.ForumAdapter.ViewName))\n\t\t\t\treturn View(adapter.ForumAdapter.Model);\n\t\t\treturn View(adapter.ForumAdapter.ViewName, adapter.ForumAdapter.Model);\n\t\t}\n\t\tif (forum.IsQAForum)\n\t\t{\n\t\t\tvar containerForQA = _forumService.MapTopicContainerForQA(container);\n\t\t\treturn View(\"TopicQA\", containerForQA);\n\t\t}\n\t\treturn View(container);\n\t}\n\n\tpublic async Task<ActionResult> TopicPage(int id, int pageNumber, int low, int high)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\treturn NotFound();\n\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"TopicID {topic.TopicID} references ForumID {topic.ForumID}, which does not exist.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user, topic);\n\t\tif (!permissionContext.UserCanView)\n\t\t{\n\t\t\treturn StatusCode(403);\n\t\t}\n\n\t\tDateTime? lastReadTime = DateTime.UtcNow;\n\t\tif (user != null)\n\t\t{\n\t\t\tlastReadTime = await _lastReadService.GetLastReadTime(user, topic);\n\t\t}\n\n\t\tvar (posts, pagerContext) = await _postService.GetPosts(topic, permissionContext.UserCanModerate, pageNumber);\n\t\tif (posts.Count == 0)\n\t\t\treturn NotFound();\n\t\tvar signatures = await _profileService.GetSignatures(posts);\n\t\tvar avatars = await _profileService.GetAvatars(posts);\n\t\tvar votedIDs = await _postService.GetVotedPostIDs(user, posts);\n\t\tvar ignores = await _ignoreService.GetIgnoredUserIdsInList(user, posts);\n\t\tvar container = ComposeTopicContainer(topic, forum, permissionContext, posts, pagerContext, signatures, avatars, votedIDs, lastReadTime, ignores);\n\t\tawait _topicViewCountService.ProcessView(topic);\n\t\tViewBag.Low = low;\n\t\tViewBag.High = high;\n\t\treturn View(container);\n\t}\n\n\tpublic async Task<ActionResult> PostReply(int id, int replyID = 0)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Content(Resources.LoginToPost);\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\treturn Content(Resources.TopicNotExist);\n\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"TopicID {topic.TopicID} references ForumID {topic.ForumID}, which does not exist.\");\n\t\tif (topic.IsClosed)\n\t\t\treturn Content(Resources.Closed);\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user, topic);\n\t\tif (!permissionContext.UserCanView)\n\t\t\treturn Content(Resources.ForumNoView);\n\t\tif (!permissionContext.UserCanPost)\n\t\t\treturn Content(Resources.ForumNoPost);\n\n\t\tvar title = topic.Title;\n\t\tif (!title.ToLower().StartsWith(\"re:\"))\n\t\t\ttitle = \"Re: \" + title;\n\t\tvar profile = await _profileService.GetProfile(user);\n\t\tvar newPost = new NewPost { ItemID = topic.TopicID, Title = title, IncludeSignature = profile.Signature.Length > 0, IsPlainText = profile.IsPlainText, IsImageEnabled = _settingsManager.Current.AllowImages, ParentPostID = replyID };\n\n\t\tif (forum.IsQAForum)\n\t\t{\n\t\t\tnewPost.IncludeSignature = false;\n\t\t\tif (newPost.ParentPostID == 0)\n\t\t\t{\n\t\t\t\tViewBag.IsQA = true;\n\t\t\t\treturn View(\"NewReply\", newPost);\n\t\t\t}\n\t\t\treturn View(\"NewComment\", newPost);\n\t\t}\n\t\treturn View(\"NewReply\", newPost);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<JsonResult> PostReply([FromBody]NewPost newPost)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar userProfileUrl = Url.Action(\"ViewProfile\", \"Account\", new { id = user.UserID });\n\t\tstring TopicLinkGenerator(Topic t) => Url.Action(\"GoToNewestPost\", Name, new { id = t.TopicID });\n\t\tstring PostLinkGenerator(Post p) => Url.Action(\"PostLink\", \"Forum\", new {id = p.PostID});\n\t\tstring RedirectLinkGenerator(Post p) => Url.RouteUrl(new {controller = \"Forum\", action = \"PostLink\", id = p.PostID});\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress?.ToString();\n\n\t\tvar result = await _postMasterService.PostReply(user, newPost.ParentPostID, ip, false, newPost, DateTime.UtcNow, TopicLinkGenerator, userProfileUrl, PostLinkGenerator, RedirectLinkGenerator);\n\n\t\treturn Json(new BasicJsonMessage { Result = result.IsSuccessful, Redirect = result.Redirect, Message = result.Message });\n\t}\n\n\tpublic async Task<ActionResult> Post(int id)\n\t{\n\t\tvar post = await _postService.Get(id);\n\t\tif (post == null)\n\t\t\treturn NotFound();\n\t\tvar (permissionContext, topic) = await GetPermissionContextByTopicID(post.TopicID);\n\t\tif (!permissionContext.UserCanView)\n\t\t\treturn StatusCode(403);\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar postList = new List<Post> { post };\n\t\tvar signatures = await _profileService.GetSignatures(postList);\n\t\tvar avatars = await _profileService.GetAvatars(postList);\n\t\tvar votedPostIDs = await _postService.GetVotedPostIDs(user, postList);\n\t\tViewData[\"PopForums.Identity.CurrentUser\"] = user; // TODO: what is this used for?\n\t\tif (user != null)\n\t\t\tawait _lastReadService.MarkTopicRead(user, topic);\n\t\tvar ignoreUserIDs = await _ignoreService.GetIgnoredUserIdsInList(user, postList);\n\t\treturn View(\"PostItem\", new PostItemContainer { Post = post, Avatars = avatars, Signatures = signatures, VotedPostIDs = votedPostIDs, Topic = topic, User = user, IgnoreUserIDs = ignoreUserIDs });\n\t}\n\t\t\n\tpublic async Task<ViewResult> Recent(int pageNumber = 1)\n\t{\n\t\tvar includeDeleted = false;\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\tincludeDeleted = true;\n\t\tvar titles = _forumService.GetAllForumTitles();\n\t\tvar (topics, pagerContext) = await _forumService.GetRecentTopics(user, includeDeleted, pageNumber);\n\t\tvar container = new PagedTopicContainer { ForumTitles = titles, PagerContext = pagerContext, Topics = topics };\n\t\tawait _lastReadService.GetTopicReadStatus(user, container);\n\t\tvar forumState = _forumStateComposer.GetState(null, pagerContext);\n\t\tViewBag.ForumState = forumState; // TODO: refactor this into the container\n\t\treturn View(container);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<RedirectToActionResult> MarkForumRead(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\tthrow new Exception(\"There is no logged in user. Can't mark forum read.\");\n\t\tvar forum = await _forumService.Get(id);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"There is no ForumID {id} to mark as read.\");\n\t\tawait _lastReadService.MarkForumRead(user, forum);\n\t\treturn RedirectToAction(\"Index\", HomeController.Name);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<RedirectToActionResult> MarkAllForumsRead()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\tthrow new Exception(\"There is no logged in user. Can't mark forum read.\");\n\t\tawait _lastReadService.MarkAllForumsRead(user);\n\t\treturn RedirectToAction(\"Index\", HomeController.Name);\n\t}\n\n\tpublic async Task<ActionResult> PostLink(int id)\n\t{\n\t\tvar includeDeleted = false;\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\tincludeDeleted = true;\n\t\tvar post = await _postService.Get(id);\n\t\tif (post == null || (post.IsDeleted && (user == null || !user.IsInRole(PermanentRoles.Moderator))))\n\t\t\treturn NotFound();\n\t\tvar (pageNumber, topic) = await _postService.GetTopicPageForPost(post, includeDeleted);\n\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\tvar adapter = new ForumAdapterFactory(forum);\n\t\tif (adapter.IsAdapterEnabled)\n\t\t{\n\t\t\tvar result = await adapter.ForumAdapter.AdaptPostLink(this, post, topic, forum);\n\t\t\tif (result != null)\n\t\t\t\treturn result;\n\t\t}\n\t\tvar url = Url.Action(\"Topic\", new { id = topic.UrlName, pageNumber }) + \"#\" + post.PostID;\n\t\treturn Redirect(url);\n\t}\n\n\tpublic async Task<ActionResult> GoToNewestPost(int id)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\treturn NotFound();\n\t\tvar includeDeleted = false;\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\tincludeDeleted = true;\n\t\tif (user == null)\n\t\t\treturn RedirectToAction(\"Topic\", new { id = topic.UrlName });\n\t\tvar post = await _lastReadService.GetFirstUnreadPost(user, topic);\n\t\tvar (pageNumber, t) = await _postService.GetTopicPageForPost(post, includeDeleted);\n\t\tvar url = Url.Action(\"Topic\", new { id = topic.UrlName, pageNumber }) + \"#\" + post.PostID;\n\t\treturn Redirect(url);\n\t}\n\n\tpublic async Task<ActionResult> Edit(int id)\n\t{\n\t\tvar post = await _postService.Get(id);\n\t\tif (post == null)\n\t\t\treturn NotFound();\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (!user.IsPostEditable(post))\n\t\t\treturn StatusCode(403);\n\t\tvar postEdit = await _postService.GetPostForEdit(post, user);\n\t\treturn View(postEdit);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> Edit(int id, PostEdit postEdit)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tpostEdit.PostImageIDs = postEdit.PostImageIDs[0]?.Split(',');\n\t\tstring RedirectLinkGenerator(Post p) => Url.RouteUrl(new { controller = \"Forum\", action = \"PostLink\", id = p.PostID });\n\t\tvar result = await _postMasterService.EditPost(id, postEdit, user, RedirectLinkGenerator);\n\t\tif (result.IsSuccessful)\n\t\t\treturn Redirect(result.Redirect);\n\t\tViewBag.Message = result.Message;\n\t\treturn View(postEdit);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> DeletePost(int id)\n\t{\n\t\tvar post = await _postService.Get(id);\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (!user.IsPostEditable(post))\n\t\t\treturn StatusCode(403);\n\t\tawait _postService.Delete(post, user);\n\t\tif (post.IsFirstInTopic || !user.IsInRole(PermanentRoles.Moderator))\n\t\t{\n\t\t\tvar topic = await _topicService.Get(post.TopicID);\n\t\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\t\treturn RedirectToAction(\"Index\", \"Forum\", new { urlName = forum.UrlName });\n\t\t}\n\t\treturn RedirectToAction(\"PostLink\", \"Forum\", new { id = post.PostID });\n\t}\n\n\tpublic async Task<ContentResult> IsLastPostInTopic(int id, int lastPostID)\n\t{\n\t\tvar last = await _postService.GetLastPostID(id);\n\t\tvar result = last == lastPostID;\n\t\treturn Content(result.ToString());\n\t}\n\n\t// use this only to load an unknown number of new posts when reply is open\n\tpublic async Task<ActionResult> TopicPartial(int id, int lastPost, int lowPage)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\treturn NotFound();\n\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"TopicID {topic.TopicID} references ForumID {topic.ForumID}, which does not exist.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\n\t\tvar permissionContext = await _forumPermissionService.GetPermissionContext(forum, user, topic);\n\t\tif (!permissionContext.UserCanView)\n\t\t{\n\t\t\treturn StatusCode(403);\n\t\t}\n\n\t\tDateTime? lastReadTime = DateTime.UtcNow;\n\t\tif (user != null)\n\t\t{\n\t\t\tlastReadTime = await _lastReadService.GetLastReadTime(user, topic);\n\t\t}\n\n\t\tvar (posts, pagerContext) = await _postService.GetPosts(topic, lastPost, permissionContext.UserCanModerate);\n\t\tvar signatures = await _profileService.GetSignatures(posts);\n\t\tvar avatars = await _profileService.GetAvatars(posts);\n\t\tvar votedIDs = await _postService.GetVotedPostIDs(user, posts);\n\t\tvar ignores = await _ignoreService.GetIgnoredUserIdsInList(user, posts);\n\t\tvar container = ComposeTopicContainer(topic, forum, permissionContext, posts, pagerContext, signatures, avatars, votedIDs, lastReadTime, ignores);\n\t\tViewBag.Low = lowPage;\n\t\tViewBag.High = pagerContext.PageCount;\n\t\treturn View(\"TopicPage\", container);\n\t}\n\n\tpublic async Task<ActionResult> Voters(int id)\n\t{\n\t\tvar post = await _postService.Get(id);\n\t\tif (post == null)\n\t\t\treturn NotFound();\n\t\tvar voters = await _postService.GetVoters(post);\n\t\treturn View(voters);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> ToggleVote(int id)\n\t{\n\t\tvar post = await _postService.Get(id);\n\t\tif (post == null)\n\t\t\treturn NotFound();\n\t\tvar topic = await _topicService.Get(post.TopicID);\n\t\tif (topic == null)\n\t\t\tthrow new Exception($\"Post {post.PostID} appears to be orphaned from a topic.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\tvar helper = Url;\n\t\tvar userProfileUrl = helper.Action(\"ViewProfile\", \"Account\", new { id = user.UserID });\n\t\tvar topicUrl = helper.Action(\"PostLink\", \"Forum\", new { id = post.PostID });\n\t\tvar result = await _postService.ToggleVoteReturnCountAndIsVoted(post, user, userProfileUrl, topicUrl, topic.Title);\n\t\tvar model = new {votes = result.Item1, isVoted = result.Item2};\n\t\treturn Json(model);\n\t}\n\n\tpublic class PreviewModel\n\t{\n\t\tpublic string FullText { get; set; }\n\t\tpublic bool IsPlainText { get; set; }\n\t}\n\n\t[HttpPost]\n\tpublic ContentResult PreviewText([FromBody] PreviewModel model)\n\t{\n\t\tvar result = _postService.GenerateParsedTextPreview(model.FullText, model.IsPlainText);\n\t\treturn Content(result, \"text/html\");\n\t}\n\n\tprivate static TopicContainer ComposeTopicContainer(Topic topic, Forum forum, ForumPermissionContext permissionContext, List<Post> posts, PagerContext pagerContext, Dictionary<int, string> signatures, Dictionary<int, int> avatars, List<int> votedPostIDs, DateTime? lastreadTime, List<int> ignoreUserIDs)\n\t{\n\t\treturn new TopicContainer { Forum = forum, Topic = topic, Posts = posts, PagerContext = pagerContext, PermissionContext = permissionContext, Signatures = signatures, Avatars = avatars, VotedPostIDs = votedPostIDs, LastReadTime = lastreadTime, IgnoreUserIDs = ignoreUserIDs };\n\t}\n\n\tpublic class SetAnswerModel\n\t{\n\t\tpublic int TopicID { get; set; }\n\t\tpublic int PostID { get; set; }\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> SetAnswer([FromBody] SetAnswerModel model)\n\t{\n\t\tvar post = await _postService.Get(model.PostID);\n\t\tif (post == null)\n\t\t\treturn NotFound();\n\t\tvar topic = await _topicService.Get(model.TopicID);\n\t\tif (topic == null)\n\t\t\treturn NotFound();\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\ttry\n\t\t{\n\t\t\tvar helper = Url;\n\t\t\tvar userProfileUrl = helper.Action(\"ViewProfile\", \"Account\", new { id = user.UserID });\n\t\t\tvar topicUrl = helper.Action(\"PostLink\", \"Forum\", new { id = post.PostID });\n\t\t\tawait _topicService.SetAnswer(user, topic, post, userProfileUrl, topicUrl);\n\t\t}\n\t\tcatch (SecurityException) // TODO: what is this?\n\t\t{\n\t\t\treturn StatusCode(403);\n\t\t}\n\t\treturn new EmptyResult();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/HomeController.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class HomeController : Controller\n{\n\tpublic HomeController(IForumService forumService, IUserService userService, IUserSessionService userSessionService, IUserRetrievalShim userRetrievalShim)\n\t{\n\t\t_forumService = forumService;\n\t\t_userService = userService;\n\t\t_userSessionService = userSessionService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t}\n\n\tpublic static string Name = \"Home\";\n\n\tprivate readonly IForumService _forumService;\n\tprivate readonly IUserService _userService;\n\tprivate readonly IUserSessionService _userSessionService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\n\tpublic async Task<ViewResult> Index()\n\t{\n\t\tViewBag.OnlineUsers = await _userService.GetUsersOnline();\n\t\tvar sessionCount = await _userSessionService.GetTotalSessionCount();\n\t\tViewBag.TotalUsers = sessionCount.ToString(\"N0\");\n\t\tViewBag.TopicCount = _forumService.GetAggregateTopicCount().Result.ToString(\"N0\");\n\t\tViewBag.PostCount = _forumService.GetAggregatePostCount().Result.ToString(\"N0\");\n\t\tvar registeredUsers = await _userService.GetTotalUsers();\n\t\tViewBag.RegisteredUsers = registeredUsers.ToString(\"N0\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tViewBag.SitemapUrl = Url.Action(\"Index\", SitemapController.Name, null, Request.Scheme);\n\t\treturn View(await _forumService.GetCategorizedForumContainerFilteredForUser(user));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/IdentityController.cs",
    "content": "﻿using System.Security.Authentication;\nusing PopForums.Mvc.Areas.Forums.Authentication;\nusing PopIdentity;\nusing PopIdentity.Providers.Facebook;\nusing PopIdentity.Providers.Google;\nusing PopIdentity.Providers.Microsoft;\nusing PopIdentity.Providers.OAuth2;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\npublic class IdentityController : Controller\n{\n\tprivate readonly ILoginLinkFactory _loginLinkFactory;\n\tprivate readonly IStateHashingService _stateHashingService;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IFacebookCallbackProcessor _facebookCallbackProcessor;\n\tprivate readonly IGoogleCallbackProcessor _googleCallbackProcessor;\n\tprivate readonly IMicrosoftCallbackProcessor _microsoftCallbackProcessor;\n\tprivate readonly IOAuth2JwtCallbackProcessor _oAuth2JwtCallbackProcessor;\n\tprivate readonly IExternalUserAssociationManager _externalUserAssociationManager;\n\tprivate readonly IUserService _userService;\n\tprivate readonly IExternalLoginTempService _externalLoginTempService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly ISecurityLogService _securityLogService;\n\tprivate readonly IOAuthOnlyService _oAuthOnlyService;\n\tprivate readonly IConfig _config;\n\n\tpublic IdentityController(ILoginLinkFactory loginLinkFactory, IStateHashingService stateHashingService, ISettingsManager settingsManager, IFacebookCallbackProcessor facebookCallbackProcessor, IGoogleCallbackProcessor googleCallbackProcessor, IMicrosoftCallbackProcessor microsoftCallbackProcessor, IOAuth2JwtCallbackProcessor oAuth2JwtCallbackProcessor, IExternalUserAssociationManager externalUserAssociationManager, IUserService userService, IExternalLoginTempService externalLoginTempService, IUserRetrievalShim userRetrievalShim, ISecurityLogService securityLogService, IOAuthOnlyService oAuthOnlyService, IConfig config)\n\t{\n\t\t_loginLinkFactory = loginLinkFactory;\n\t\t_stateHashingService = stateHashingService;\n\t\t_settingsManager = settingsManager;\n\t\t_facebookCallbackProcessor = facebookCallbackProcessor;\n\t\t_googleCallbackProcessor = googleCallbackProcessor;\n\t\t_microsoftCallbackProcessor = microsoftCallbackProcessor;\n\t\t_oAuth2JwtCallbackProcessor = oAuth2JwtCallbackProcessor;\n\t\t_externalUserAssociationManager = externalUserAssociationManager;\n\t\t_userService = userService;\n\t\t_externalLoginTempService = externalLoginTempService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_securityLogService = securityLogService;\n\t\t_oAuthOnlyService = oAuthOnlyService;\n\t\t_config = config;\n\t}\n\n\tpublic static string Name = \"Identity\";\n\n\tpublic class Credentials\n\t{\n\t\tpublic string Email { get; set; }\n\t\tpublic string Password { get; set; }\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic async Task<IActionResult> Login([FromBody] Credentials credentials)\n\t{\n\t\tvar (result, user) = await _userService.Login(credentials.Email, credentials.Password, HttpContext.Connection.RemoteIpAddress?.ToString());\n\t\tif (result)\n\t\t{\n\t\t\tawait PerformSignInAsync(user, HttpContext);\n\t\t\treturn Json(new BasicJsonMessage { Result = true });\n\t\t}\n\n\t\treturn Json(new BasicJsonMessage { Result = false, Message = Resources.LoginBad });\n\t}\n\n\t[HttpGet]\n\tpublic async Task<RedirectResult> Logout()\n\t{\n\t\tstring link;\n\t\tif (Request == null || string.IsNullOrWhiteSpace(Request.Headers[\"Referer\"]))\n\t\t\tlink = Url.Action(\"Index\", HomeController.Name);\n\t\telse\n\t\t{\n\t\t\tlink = Request.Headers[\"Referer\"];\n\t\t\tif (!link.Contains(Request.Host.Value))\n\t\t\t\tlink = Url.Action(\"Index\", HomeController.Name);\n\t\t}\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tawait _userService.Logout(user, HttpContext.Connection.RemoteIpAddress?.ToString());\n\t\tawait HttpContext.SignOutAsync(PopForumsAuthenticationDefaults.AuthenticationScheme);\n\t\treturn Redirect(link);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<JsonResult> LogoutAsync()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tawait _userService.Logout(user, HttpContext.Connection.RemoteIpAddress?.ToString());\n\t\tawait HttpContext.SignOutAsync(PopForumsAuthenticationDefaults.AuthenticationScheme);\n\t\treturn Json(new BasicJsonMessage { Result = true });\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic IActionResult ExternalLogin(string provider, string returnUrl)\n\t{\n\t\tvar state = _stateHashingService.SetCookieAndReturnHash();\n\t\tstring redirect;\n\t\tProviderType providerType;\n\t\tswitch (provider.ToLower())\n\t\t{\n\t\t\tcase \"facebook\":\n\t\t\t\tvar facebookRedirect = Url.Action(nameof(CallbackHandler), Name, null, Request.Scheme);\n\t\t\t\tredirect = _loginLinkFactory.GetLink(ProviderType.Facebook, facebookRedirect, state, _settingsManager.Current.FacebookAppID);\n\t\t\t\tproviderType = ProviderType.Facebook;\n\t\t\t\tbreak;\n\t\t\tcase \"google\":\n\t\t\t\tvar googleRedirect = Url.Action(nameof(CallbackHandler), Name, null, Request.Scheme);\n\t\t\t\tredirect = _loginLinkFactory.GetLink(ProviderType.Google, googleRedirect, state, _settingsManager.Current.GoogleClientId);\n\t\t\t\tproviderType = ProviderType.Google;\n\t\t\t\tbreak;\n\t\t\tcase \"microsoft\":\n\t\t\t\tvar msftRedirect = Url.Action(nameof(CallbackHandler), Name, null, Request.Scheme);\n\t\t\t\tredirect = _loginLinkFactory.GetLink(ProviderType.Microsoft, msftRedirect, state, _settingsManager.Current.MicrosoftClientID);\n\t\t\t\tproviderType = ProviderType.Microsoft;\n\t\t\t\tbreak;\n\t\t\tcase \"oauth2\":\n\t\t\t\tvar oauthRedirect = Url.Action(nameof(CallbackHandler), Name, null, Request.Scheme);\n\t\t\t\tvar linkGenerator = new OAuth2LoginUrlGenerator();\n\t\t\t\tvar oauthClaims = new List<string>(new[] { \"openid\", \"email\" });\n\t\t\t\tredirect = linkGenerator.GetUrl(_settingsManager.Current.OAuth2LoginUrl, _settingsManager.Current.OAuth2ClientID, oauthRedirect, state, oauthClaims);\n\t\t\t\tproviderType = ProviderType.OAuth2;\n\t\t\t\tbreak;\n\t\t\tdefault: throw new NotImplementedException($\"The external login \\\"{provider}\\\" is not configured.\");\n\t\t}\n\n\t\tvar loginState = new ExternalLoginState {ProviderType = providerType, ReturnUrl = returnUrl };\n\t\t_externalLoginTempService.Persist(loginState);\n\t\treturn Redirect(redirect);\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic async Task<IActionResult> ExternalLoginCallback(string returnUrl = null)\n\t{\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress?.ToString();\n\t\tvar loginState = _externalLoginTempService.Read();\n\t\tif (loginState == null)\n\t\t{\n\t\t\tawait _securityLogService.CreateLogEntry((User)null, null, ip, \"Temp auth cookie missing on callback\", SecurityLogType.ExternalAssociationCheckFailed);\n\t\t\treturn View(\"ExternalError\", Resources.LoginBad);\n\t\t}\n\t\tvar externalLoginInfo = new ExternalLoginInfo(loginState.ProviderType.ToString(), loginState.ResultData.ID, loginState.ResultData.Name);\n\t\tvar matchResult = await _externalUserAssociationManager.ExternalUserAssociationCheck(externalLoginInfo, ip);\n\t\tif (matchResult.Successful)\n\t\t{\n\t\t\tawait _userService.Login(matchResult.User, ip);\n\t\t\t_externalLoginTempService.Remove();\n\t\t\tif (loginState.ProviderType == ProviderType.OAuthOnly)\n\t\t\t\tawait PerformSignInAsync(matchResult.User, HttpContext, DateTime.UtcNow.AddMinutes(_config.OAuthRefreshExpirationMinutes));\n\t\t\telse\n\t\t\t\tawait PerformSignInAsync(matchResult.User, HttpContext);\n\t\t\tif (!Url.IsLocalUrl(returnUrl))\n\t\t\t\treturnUrl = Url.Action(nameof(HomeController.Index), HomeController.Name);\n\t\t\treturn Redirect(returnUrl);\n\t\t}\n\n\t\tif (loginState.ProviderType == ProviderType.OAuthOnly)\n\t\t\tthrow new AuthenticationException(\n\t\t\t\t\"A callback was made to login in IsOAuthOnly mode, but no user was found.\");\n\t\tViewBag.Referrer = returnUrl;\n\t\treturn View();\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[TypeFilter(typeof(OAuthOnlyForbidAttribute))]\n\t[HttpPost]\n\tpublic async Task<IActionResult> LoginAndAssociate([FromBody] Credentials credentials)\n\t{\n\t\tif (_config.IsOAuthOnly)\n\t\t\treturn Forbid();\n\t\t\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress?.ToString();\n\t\tvar (result, user) = await _userService.Login(credentials.Email, credentials.Password, ip);\n\t\tif (result)\n\t\t{\n\t\t\tvar loginState = _externalLoginTempService.Read();\n\t\t\tif (loginState != null)\n\t\t\t{\n\t\t\t\tvar externalLoginInfo = new ExternalLoginInfo(loginState.ProviderType.ToString(), loginState.ResultData.ID, loginState.ResultData.Name);\n\t\t\t\tawait _externalUserAssociationManager.Associate(user, externalLoginInfo, ip);\n\t\t\t\t_externalLoginTempService.Remove();\n\t\t\t\tawait PerformSignInAsync(user, HttpContext);\n\t\t\t\treturn Json(new BasicJsonMessage { Result = true });\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tawait _securityLogService.CreateLogEntry((User)null, null, ip, \"Temp auth cookie missing on association\", SecurityLogType.ExternalAssociationCheckFailed);\n\t\t\t\treturn Json(new BasicJsonMessage { Result = false, Message = Resources.Error + \": \" + Resources.LoginBad });\n\t\t\t}\n\t\t}\n\t\treturn Json(new BasicJsonMessage { Result = false, Message = Resources.LoginBad });\n\t}\n\n\tpublic static async Task PerformSignInAsync(User user, HttpContext httpContext)\n\t{\n\t\tvar expiration = DateTime.UtcNow.AddYears(1);\n\t\tawait PerformSignInAsync(user, httpContext, expiration);\n\t}\n\n\tpublic static async Task PerformSignInAsync(User user, HttpContext httpContext, DateTime expiration)\n\t{\n\t\tvar claims = new List<Claim>\n\t\t{\n\t\t\tnew (ClaimTypes.Name, user.Name)\n\t\t};\n\n\t\tvar props = new AuthenticationProperties\n\t\t{\n\t\t\tIsPersistent = true,\n\t\t\tExpiresUtc = expiration\n\t\t};\n\n\t\tvar id = new ClaimsIdentity(claims, PopForumsAuthenticationDefaults.AuthenticationScheme);\n\t\tawait httpContext.SignInAsync(PopForumsAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(id), props);\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic async Task<IActionResult> CallbackHandler()\n\t{\n\t\tvar loginState = _externalLoginTempService.Read();\n\t\tvar ip = HttpContext.Connection.RemoteIpAddress?.ToString();\n\t\tif (loginState == null)\n\t\t{\n\t\t\tawait _securityLogService.CreateLogEntry((User)null, null, ip, \"Temp auth cookie missing on callback\", SecurityLogType.ExternalAssociationCheckFailed);\n\t\t\treturn View(\"ExternalError\", Resources.Error + \": \" + Resources.LoginBad);\n\t\t}\n\t\tvar redirectUri = Url.Action(nameof(CallbackHandler), Name, null, Request.Scheme);\n\t\tCallbackResult result;\n\t\tswitch (loginState.ProviderType)\n\t\t{\n\t\t\tcase ProviderType.OAuthOnly:\n\t\t\t\tresult = await _oAuthOnlyService.ProcessOAuthLogin(redirectUri, ip);\n\t\t\t\tif (result.IsSuccessful)\n\t\t\t\t{\n\t\t\t\t\tloginState.Expiration = result.Token.ValidTo;\n\t\t\t\t\tloginState.ReturnUrl = null;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase ProviderType.Facebook:\n\t\t\t\tresult = await _facebookCallbackProcessor.VerifyCallback(redirectUri, _settingsManager.Current.FacebookAppID, _settingsManager.Current.FacebookAppSecret);\n\t\t\t\tbreak;\n\t\t\tcase ProviderType.Google:\n\t\t\t\tresult = await _googleCallbackProcessor.VerifyCallback(redirectUri, _settingsManager.Current.GoogleClientId, _settingsManager.Current.GoogleClientSecret);\n\t\t\t\tbreak;\n\t\t\tcase ProviderType.Microsoft:\n\t\t\t\tresult = await _microsoftCallbackProcessor.VerifyCallback(redirectUri, _settingsManager.Current.MicrosoftClientID, _settingsManager.Current.MicrosoftClientSecret);\n\t\t\t\tbreak;\n\t\t\tcase ProviderType.OAuth2:\n\t\t\t\tresult = await _oAuth2JwtCallbackProcessor.VerifyCallback(redirectUri, _settingsManager.Current.OAuth2TokenUrl, _settingsManager.Current.OAuth2ClientID, _settingsManager.Current.OAuth2ClientSecret);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Exception($\"The external login type {loginState.ProviderType} has no callback handler.\");\n\t\t}\n\t\tif (!result.IsSuccessful)\n\t\t{\n\t\t\treturn View(\"ExternalError\", result.Message);\n\t\t}\n\t\tloginState.ResultData = result.ResultData;\n\t\t_externalLoginTempService.Persist(loginState);\n\n\t\treturn RedirectToAction(\"ExternalLoginCallback\", new { returnUrl = loginState.ReturnUrl });\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/IgnoreController.cs",
    "content": "namespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\npublic class IgnoreController(IIgnoreService ignoreService, IUserRetrievalShim userRetrievalShim) : Controller\n{\n\tpublic static readonly string Name = \"Ignore\";\n\t\n\t[HttpPost]\n\tpublic async Task<IActionResult> Add(int userID, int postID)\n\t{\n\t\tvar user = userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Forbid();\n\t\tawait ignoreService.AddIgnore(user.UserID, userID);\n\t\treturn RedirectToAction(\"PostLink\", \"Forum\", new { id = postID });\n\t}\n\t\n\t[HttpPost]\n\tpublic async Task<IActionResult> Remove(int userID, int postID)\n\t{\n\t\tvar user = userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Forbid();\n\t\tawait ignoreService.DeleteIgnore(user.UserID, userID);\n\t\treturn RedirectToAction(\"PostLink\", \"Forum\", new { id = postID });\n\t}\n\t\n\t[HttpPost]\n\tpublic async Task<IActionResult> RemoveFromList(int id)\n\t{\n\t\tvar user = userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Forbid();\n\t\tawait ignoreService.DeleteIgnore(user.UserID, id);\n\t\treturn RedirectToAction(\"List\");\n\t}\n\n\tpublic async Task<IActionResult> List()\n\t{\n\t\tvar user = userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View(null);\n\t\tvar list = await ignoreService.GetIgnoreList(user.UserID);\n\t\treturn View(list);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/ImageController.cs",
    "content": "﻿using System.Net.Mime;\nusing PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class ImageController : Controller\n{\n\tpublic ImageController(IImageService imageService, IUserRetrievalShim userRetrievalShim, IPostImageService postImageService, ISettingsManager settingsManager)\n\t{\n\t\t_imageService = imageService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_postImageService = postImageService;\n\t\t_settingsManager = settingsManager;\n\t}\n\n\tprivate readonly IImageService _imageService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly IPostImageService _postImageService;\n\tprivate readonly ISettingsManager _settingsManager;\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic async Task<ActionResult> Avatar(int id)\n\t{\n\t\treturn await SetupImageResult(_imageService.GetAvatarImageStream, _imageService.GetAvatarImageLastModification, id);\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic async Task<ActionResult> UserImage(int id)\n\t{\n\t\treturn await SetupImageResult(_imageService.GetUserImageStream, _imageService.GetUserImageLastModifcation, id);\n\t}\n\n\tprivate async Task<ActionResult> SetupImageResult(Func<int, Task<IStreamResponse>> imageStreamFetch, Func<int, Task<DateTime?>> imageLastMod, int id)\n\t{\n\t\tvar timeStamp = await imageLastMod(id);\n\t\tif (!timeStamp.HasValue)\n\t\t\treturn NotFound();\n\t\tResponse.Headers[\"Cache-control\"] = \"private\";\n\t\tResponse.Headers[\"Last-modified\"] = DateTime.SpecifyKind(timeStamp.Value, DateTimeKind.Utc).ToString(\"R\");\n\t\tif (!string.IsNullOrEmpty(Request.Headers[\"If-Modified-Since\"]))\n\t\t{\n\t\t\tvar provider = CultureInfo.InvariantCulture;\n\t\t\tvar couldParse = DateTime.TryParseExact(Request.Headers[\"If-Modified-Since\"], \"r\", provider, DateTimeStyles.None, out var lastMod);\n\t\t\tif (couldParse && lastMod == timeStamp.Value.AddMilliseconds(-timeStamp.Value.Millisecond))\n\t\t\t{\n\t\t\t\tResponse.StatusCode = 304;\n\t\t\t\treturn Content(string.Empty);\n\t\t\t}\n\t\t}\n\t\tvar streamResponse = await imageStreamFetch(id);\n\t\tif (streamResponse == null)\n\t\t\treturn NotFound();\n\t\tResponse.RegisterForDispose(streamResponse);\n\t\treturn File(streamResponse.Stream, \"image/jpeg\");\n\t}\n\n\tpublic async Task<ActionResult> PostImage(string id)\n\t{\n\t\tvar postImageSansData = await _postImageService.GetWithoutData(id);\n\t\tif (postImageSansData == null)\n\t\t\treturn NotFound();\n\t\tResponse.Headers[\"Cache-control\"] = \"private\";\n\t\tResponse.Headers[\"Last-modified\"] = DateTime.SpecifyKind(postImageSansData.TimeStamp, DateTimeKind.Utc).ToString(\"R\");\n\t\tif (!string.IsNullOrEmpty(Request.Headers[\"If-Modified-Since\"]))\n\t\t{\n\t\t\tvar provider = CultureInfo.InvariantCulture;\n\t\t\tvar couldParse = DateTime.TryParseExact(Request.Headers[\"If-Modified-Since\"], \"r\", provider, DateTimeStyles.None, out var lastMod);\n\t\t\tif (couldParse && lastMod == postImageSansData.TimeStamp.AddMilliseconds(-postImageSansData.TimeStamp.Millisecond))\n\t\t\t{\n\t\t\t\tResponse.StatusCode = 304;\n\t\t\t\treturn Content(string.Empty);\n\t\t\t}\n\t\t}\n\t\t\n\t\tvar streamResponse = await _postImageService.GetImageStream(id);\n\t\tif (streamResponse == null)\n\t\t\treturn NotFound();\n\t\tResponse.RegisterForDispose(streamResponse);\n\t\treturn File(streamResponse.Stream, postImageSansData.ContentType);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> UploadPostImage()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Unauthorized();\n\t\tif (!_settingsManager.Current.AllowImages)\n\t\t\treturn BadRequest();\n\t\tvar file = Request.Form.Files[0];\n\t\tif (file.ContentType != MediaTypeNames.Image.Jpeg \n\t\t    && file.ContentType != MediaTypeNames.Image.Gif\n\t\t    && file.ContentType != \"image/png\")\n\t\t\treturn BadRequest();\n\t\tvar stream = file.OpenReadStream();\n\t\tvar bytes = stream.ToBytes();\n\t\tvar isOk = _postImageService.ProcessImageIsOk(bytes, file.ContentType);\n\t\tif (!isOk)\n\t\t\treturn BadRequest();\n\t\tvar url = await _postImageService.PersistAndGetPayload();\n\t\treturn Ok(url);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/ModeratorController.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Authorize(Policy = PermanentRoles.Moderator)]\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class ModeratorController : Controller\n{\n\tpublic ModeratorController(ITopicService topicService, IForumService forumService, IPostService postService, IModerationLogService moderationLogService, IUserRetrievalShim userRetrievalShim)\n\t{\n\t\t_topicService = topicService;\n\t\t_forumService = forumService;\n\t\t_postService = postService;\n\t\t_moderationLogService = moderationLogService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t}\n\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly IPostService _postService;\n\tprivate readonly IModerationLogService _moderationLogService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\n\t[HttpPost]\n\tpublic async Task<RedirectToActionResult> TogglePin(int id)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\tthrow new Exception($\"Topic with ID {id} not found. Can't pin/unpin.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (topic.IsPinned)\n\t\t\tawait _topicService.UnpinTopic(topic, user);\n\t\telse\n\t\t\tawait _topicService.PinTopic(topic, user);\n\t\treturn RedirectToAction(\"Topic\", \"Forum\", new { id = topic.UrlName });\n\t}\n\n\t[HttpPost]\n\tpublic async Task<RedirectToActionResult> ToggleClosed(int id)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\tthrow new Exception($\"Topic with ID {id} not found. Can't open/close.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (topic.IsClosed)\n\t\t\tawait _topicService.OpenTopic(topic, user);\n\t\telse\n\t\t\tawait _topicService.CloseTopic(topic, user);\n\t\treturn RedirectToAction(\"Topic\", \"Forum\", new { id = topic.UrlName });\n\t}\n\n\t[HttpPost]\n\tpublic async Task<RedirectToActionResult> ToggleDeleted(int id)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\tthrow new Exception($\"Topic with ID {id} not found. Can't delete/undelete.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (topic.IsDeleted)\n\t\t\tawait _topicService.UndeleteTopic(topic, user);\n\t\telse\n\t\t\tawait _topicService.DeleteTopic(topic, user);\n\t\treturn RedirectToAction(\"Topic\", \"Forum\", new { id = topic.UrlName });\n\t}\n\n\t[HttpPost]\n\tpublic async Task<RedirectToActionResult> UpdateTopic(IFormCollection collection)\n\t{\n\t\tint topicID;\n\t\tif (!int.TryParse(collection[\"TopicID\"], out topicID))\n\t\t\tthrow new Exception(\"Parse TopicID fail.\");\n\t\tvar topic = await _topicService.Get(topicID);\n\t\tif (topic == null)\n\t\t\tthrow new Exception($\"Topic with ID {topicID} not found. Can't update.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar newTitle = collection[\"NewTitle\"];\n\t\tint forumID;\n\t\tif (!int.TryParse(collection[\"NewForum\"], out forumID))\n\t\t\tthrow new Exception(\"Parse ForumID fail.\");\n\t\tvar forum = await _forumService.Get(forumID);\n\t\tif (forum == null)\n\t\t\tthrow new Exception($\"Forum with ID {forumID} not found. Can't update.\");\n\t\tawait _topicService.UpdateTitleAndForum(topic, forum, newTitle, user);\n\t\treturn RedirectToAction(\"Topic\", \"Forum\", new { id = topic.UrlName });\n\t}\n\n\t[HttpPost]\n\tpublic async Task<RedirectToActionResult> UndeletePost(int id)\n\t{\n\t\tvar post = await _postService.Get(id);\n\t\tif (post == null)\n\t\t\tthrow new Exception($\"Post with ID {id} not found. Can't undelete.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tawait _postService.Undelete(post, user);\n\t\treturn RedirectToAction(\"PostLink\", \"Forum\", new { id = post.PostID });\n\t}\n\n\tpublic async Task<ViewResult> TopicModerationLog(int id)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\tthrow new Exception($\"There is no topic with ID {id} to obtain a moderation log for.\");\n\t\tvar log = await _moderationLogService.GetLog(topic, true);\n\t\treturn View(log);\n\t}\n\n\tpublic async Task<ViewResult> PostModerationLog(int id)\n\t{\n\t\tvar post = await _postService.Get(id);\n\t\tif (post == null)\n\t\t\tthrow new Exception($\"There is no post with ID {id} to obtain a moderation log for.\");\n\t\tvar log = await _moderationLogService.GetLog(post);\n\t\treturn View(log);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<RedirectToActionResult> DeleteTopicPermanently(int id)\n\t{\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\tthrow new Exception($\"Topic with ID {id} not found. Can't undelete.\");\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar forum = await _forumService.Get(topic.ForumID);\n\t\tawait _topicService.HardDeleteTopic(topic, user);\n\t\treturn RedirectToAction(\"Index\", \"Forum\", new { urlName = forum.UrlName });\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/PrivateMessagesController.cs",
    "content": "﻿using PopForums.Composers;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class PrivateMessagesController : Controller\n{\n\tpublic PrivateMessagesController(IPrivateMessageService privateMessageService, IUserService userService, IUserRetrievalShim userRetrievalShim, IPrivateMessageStateComposer privateMessageStateComposer)\n\t{\n\t\t_privateMessageService = privateMessageService;\n\t\t_userService = userService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_privateMessageStateComposer = privateMessageStateComposer;\n\t}\n\n\tprivate readonly IPrivateMessageService _privateMessageService;\n\tprivate readonly IUserService _userService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly IPrivateMessageStateComposer _privateMessageStateComposer;\n\n\tpublic static string Name = \"PrivateMessages\";\n\n\tpublic async Task<ActionResult> Index(int pageNumber = 1)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\tvar (privateMessages, pagerContext) = await _privateMessageService.GetPrivateMessages(user, PrivateMessageBoxType.Inbox, pageNumber);\n\t\tViewBag.PagerContext = pagerContext;\n\t\treturn View(privateMessages);\n\t}\n\n\tpublic async Task<ActionResult> Archive(int pageNumber = 1)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\tvar (privateMessages, pagerContext) = await _privateMessageService.GetPrivateMessages(user, PrivateMessageBoxType.Archive, pageNumber);\n\t\tViewBag.PagerContext = pagerContext;\n\t\treturn View(privateMessages);\n\t}\n\n\tpublic async Task<ActionResult> View(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\tvar pm = await _privateMessageService.Get(id, user.UserID);\n\t\tif (await _privateMessageService.IsUserInPM(user.UserID, id) == false)\n\t\t\treturn StatusCode(403);\n\t\tvar state = await _privateMessageStateComposer.GetState(pm);\n\t\tvar model = new PrivateMessageView\n\t\t{\n\t\t\tPrivateMessage = pm,\n\t\t\tState = state\n\t\t};\n\t\tawait _privateMessageService.MarkPMRead(user.UserID, id);\n\t\treturn View(model);\n\t}\n\n\tpublic async Task<ActionResult> Create(int? id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\tViewBag.UserIDs = \" \";\n\t\tif (id.HasValue && id != user.UserID)\n\t\t{\n\t\t\tvar targetUser = await _userService.GetUser(id.Value);\n\t\t\tViewBag.UserIDs = targetUser.UserID.ToString(CultureInfo.InvariantCulture);\n\t\t\tViewBag.UserID = targetUser.UserID.ToString(CultureInfo.InvariantCulture);\n\t\t\tViewBag.TargetUserID = targetUser.UserID;\n\t\t\tViewBag.TargetUserName = targetUser.Name;\n\t\t}\n\t\treturn View();\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> Create(string fullText, string userIDs)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\tif (string.IsNullOrWhiteSpace(userIDs) || string.IsNullOrWhiteSpace(fullText))\n\t\t{\n\t\t\tViewBag.Warning = Resources.PMCreateWarnings;\n\t\t\treturn View(\"Create\");\n\t\t}\n\t\tvar ids = userIDs.Split(new[] { ',' }).Select(i => Convert.ToInt32(i));\n\t\tif (ids.Count() > 10)\n\t\t\tids = ids.Take(10);\n\t\tvar users = ids.Select(id => _userService.GetUser(id).Result).ToList();\n\t\tvar pm = await _privateMessageService.Create(fullText, user, users);\n\t\treturn RedirectToAction(\"View\", new { id = pm.PMID });\n\t}\n\n\tpublic async Task<JsonResult> GetNames(string id)\n\t{\n\t\tvar users = await _userService.SearchByName(id);\n\t\tvar projection = users.Select(u => new { u.UserID, value = u.Name });\n\t\treturn Json(projection);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> ArchivePM(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\tvar pm = await _privateMessageService.Get(id, user.UserID);\n\t\tif (await _privateMessageService.IsUserInPM(user.UserID, pm.PMID) == false)\n\t\t\treturn StatusCode(403);\n\t\tawait _privateMessageService.Archive(user, pm);\n\t\treturn RedirectToAction(\"Index\");\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> UnarchivePM(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn StatusCode(403);\n\t\tvar pm = await _privateMessageService.Get(id, user.UserID);\n\t\tif (await _privateMessageService.IsUserInPM(user.UserID, pm.PMID) == false)\n\t\t\treturn StatusCode(403);\n\t\tawait _privateMessageService.Unarchive(user, pm);\n\t\treturn RedirectToAction(\"Archive\");\n\t}\n\n\tpublic async Task<ContentResult> NewPMCount()\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Content(String.Empty);\n\t\tvar count = await _privateMessageService.GetUnreadCount(user.UserID);\n\t\treturn Content(count.ToString());\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/ResourcesController.cs",
    "content": "﻿using PopForums.Composers;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\npublic class ResourcesController : Controller\n{\n\tprivate readonly IResourceComposer _resourceComposer;\n\n\tpublic ResourcesController(IResourceComposer resourceComposer)\n\t{\n\t\t_resourceComposer = resourceComposer;\n\t}\n\n\t[ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)]\n\tpublic JsonResult Index()\n\t{\n\t\tvar result = _resourceComposer.GetForCurrentThread();\n\t\treturn Json(result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/SearchController.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class SearchController : Controller\n{\n\tpublic SearchController(ISearchService searchService, IForumService forumService, ILastReadService lastReadService, IUserRetrievalShim userRetrievalShim)\n\t{\n\t\t_searchService = searchService;\n\t\t_forumService = forumService;\n\t\t_lastReadService = lastReadService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t}\n\n\tprivate readonly ISearchService _searchService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly ILastReadService _lastReadService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\n\tpublic static string Name = \"Search\";\n\n\tpublic ViewResult Index()\n\t{\n\t\tvar container = new PagedTopicContainer { PagerContext = new PagerContext { PageCount = 0, PageIndex = 1 }, Topics = new List<Topic>() };\n\t\tViewBag.SearchTypes = new SelectList(Enum.GetValues(typeof(SearchType)));\n\t\treturn View(container);\n\t}\n\n\t[HttpPost]\n\tpublic ActionResult Process(IFormCollection collection)\n\t{\n\t\tvar query = Request.Form[\"Query\"];\n\t\tvar searchType = Request.Form[\"SearchType\"];\n\t\treturn RedirectToAction(\"Result\", new { query, searchType });\n\t}\n\t\t\n\tpublic async Task<ViewResult> Result(string query, SearchType searchType = SearchType.Rank, int pageNumber = 1)\n\t{\n\t\tViewBag.SearchTypes = new SelectList(Enum.GetValues(typeof(SearchType)));\n\t\tViewBag.Query = query;\n\t\tViewBag.SearchType = searchType;\n\t\tvar includeDeleted = false;\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\tincludeDeleted = true;\n\t\tvar titles = _forumService.GetAllForumTitles();\n\t\tvar (topics, pagerContext) = await _searchService.GetTopics(query, searchType, user, includeDeleted, pageNumber);\n\t\tvar container = new PagedTopicContainer { ForumTitles = titles, PagerContext = pagerContext, Topics = topics.Data };\n\t\tViewBag.IsError = !topics.IsValid;\n\t\tif (topics.IsValid)\n\t\t\tawait _lastReadService.GetTopicReadStatus(user, container);\n\t\treturn View(\"Index\", container);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/SetupController.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\npublic class SetupController : Controller\n{\n\tpublic SetupController(ISetupService setupService, IConfig config)\n\t{\n\t\t_setupService = setupService;\n\t\t_config = config;\n\t}\n\n\tprivate readonly ISetupService _setupService;\n\tprivate readonly IConfig _config;\n\n\tpublic static string Name = \"Setup\";\n\n\t[PopForumsAuthenticationIgnore]\n\tpublic ActionResult Index()\n\t{\n\t\tif (_config.IsOAuthOnly)\n\t\t\treturn RedirectToAction(nameof(OAuthOnlySetup));\n\t\tif (!_setupService.IsConnectionPossible())\n\t\t\treturn View(\"NoConnection\");\n\t\tif (_setupService.IsDatabaseSetup())\n\t\t\treturn StatusCode(403);\n\t\tvar setupVariables = new SetupVariables\n\t\t{\n\t\t\tSmtpPort = 25\n\t\t};\n\t\treturn View(setupVariables);\n\t}\n\n\tpublic IActionResult OAuthOnlySetup()\n\t{\n\t\tif (!_setupService.IsConnectionPossible())\n\t\t\treturn View(\"NoConnection\");\n\t\tif (_setupService.IsDatabaseSetup())\n\t\t\treturn StatusCode(403);\n\t\tvar exception = _setupService.SetupDatabaseWithoutSettingsOrUser();\n\t\tif (exception != null)\n\t\t\treturn View(\"Exception\");\n\t\treturn View(\"Success\");\n\t}\n\n\t[PopForumsAuthenticationIgnore]\n\t[HttpPost]\n\tpublic async Task<ActionResult> Index(SetupVariables setupVariables)\n\t{\n\t\tif (_setupService.IsDatabaseSetup())\n\t\t\treturn StatusCode(403);\n\t\tvar result = await _setupService.SetupDatabase(setupVariables);\n\t\tif (result.Item2 != null)\n\t\t\treturn View(\"Exception\", result.Item2);\n\t\t// can't login here because all of the normal app startup was skipped\n\t\t//await AuthorizationController.PerformSignInAsync(true, user, HttpContext);\n\t\treturn View(\"Success\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/SitemapController.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class SitemapController : Controller\n{\n\tprivate readonly ISitemapService _sitemapService;\n\n\tpublic static string Name = \"Sitemap\";\n\n\tpublic SitemapController(ISitemapService sitemapService)\n\t{\n\t\t_sitemapService = sitemapService;\n\t}\n\n\t[HttpGet(\"/Forums/Sitemap.xml\")]\n\t[ResponseCache(Duration = 900)]\n\tpublic async Task<IActionResult> Index()\n\t{\n\t\tstring SitemapPageLinkGenerator(int page) => Url.Action(\"Page\", Name, new { page }, Request.Scheme);\n\t\tvar sitemapIndex = await _sitemapService.GenerateIndex(SitemapPageLinkGenerator);\n\t\treturn Content(sitemapIndex, \"text/xml\");\n\t}\n\n\t[HttpGet(\"/Forums/Sitemap.{page}.xml\")]\n\t[ResponseCache(Duration = 900)]\n\tpublic async Task<IActionResult> Page(int page)\n\t{\n\t\tstring TopicLinkGenerator(string id) => Url.Action(\"Topic\", ForumController.Name, new { id }, Request.Scheme);\n\t\tvar sitemap = await _sitemapService.GeneratePage(TopicLinkGenerator, page);\n\t\treturn Content(sitemap, \"text/xml\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Controllers/SubscriptionController.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Controllers;\n\n[Area(\"Forums\")]\n[TypeFilter(typeof(PopForumsPrivateForumsFilter))]\npublic class SubscriptionController : Controller\n{\n\tpublic SubscriptionController(ISubscribedTopicsService subService, ITopicService topicService, IUserService userService, ILastReadService lastReadService, IForumService forumService, IUserRetrievalShim userRetrievalShim, IProfileService profileService)\n\t{\n\t\t_subService = subService;\n\t\t_topicService = topicService;\n\t\t_userService = userService;\n\t\t_lastReadService = lastReadService;\n\t\t_forumService = forumService;\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_profileService = profileService;\n\t}\n\n\tpublic static string Name = \"Subscription\";\n\n\tprivate readonly ISubscribedTopicsService _subService;\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly IUserService _userService;\n\tprivate readonly ILastReadService _lastReadService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly IProfileService _profileService;\n\n\tpublic async Task<ViewResult> Topics(int pageNumber = 1)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn View();\n\t\tvar (topics, pagerContext) = await _subService.GetTopics(user, pageNumber);\n\t\tvar titles = _forumService.GetAllForumTitles();\n\t\tvar container = new PagedTopicContainer { PagerContext = pagerContext, Topics = topics, ForumTitles = titles };\n\t\tawait _lastReadService.GetTopicReadStatus(user, container);\n\t\treturn View(container);\n\t}\n\n\t[HttpPost]\n\tpublic async Task<ActionResult> Unsubscribe(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tvar topic = await _topicService.Get(id);\n\t\tawait _subService.TryRemoveSubscribedTopic(user, topic);\n\t\treturn RedirectToAction(\"Topics\");\n\t}\n\n\t[HttpPost]\n\tpublic async Task<JsonResult> ToggleSubscription(int id)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn Json(new BasicJsonMessage { Message = Resources.LoginToPost, Result = false });\n\t\tvar topic = await _topicService.Get(id);\n\t\tif (topic == null)\n\t\t\treturn Json(new BasicJsonMessage { Message = Resources.TopicNotExist, Result = false });\n\t\tif (await _subService.IsTopicSubscribed(user.UserID, topic.TopicID))\n\t\t{\n\t\t\tawait _subService.RemoveSubscribedTopic(user, topic);\n\t\t\treturn Json(new BasicJsonMessage { Data = new { isSubscribed = false }, Result = true });\n\t\t}\n\t\tawait _subService.AddSubscribedTopic(user.UserID, topic.TopicID);\n\t\treturn Json(new BasicJsonMessage { Data = new { isSubscribed = true }, Result = true });\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Extensions/ApplicationBuilders.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Extensions;\n\npublic static class ApplicationBuilders\n{\n\t/// <summary>\n\t/// Enables the POP Forums middleware to identify PF users.\n\t/// </summary>\n\tpublic static IApplicationBuilder UsePopForumsAuth(this IApplicationBuilder app)\n\t{\n\t\tapp.UseMiddleware<PopForumsAuthenticationMiddleware>();\n\t\treturn app;\n\t}\n\n\t/// <summary>\n\t/// Enables the localization (languages) for POP Forums. Call this before UseMvc.\n\t/// </summary>\n\tpublic static IApplicationBuilder UsePopForumsCultures(this IApplicationBuilder app)\n\t{\n\t\tvar supportedCultures = new List<CultureInfo> { new CultureInfo(\"en\"), new CultureInfo(\"de\"), new CultureInfo(\"es\"), new CultureInfo(\"nl\"), new CultureInfo(\"uk\"), new CultureInfo(\"zh-TW\") };\n\t\tapp.UseRequestLocalization(new RequestLocalizationOptions\n\t\t{\n\t\t\tDefaultRequestCulture = new RequestCulture(\"en\", \"en\"),\n\t\t\tSupportedCultures = supportedCultures,\n\t\t\tSupportedUICultures = supportedCultures\n\t\t});\n\t\treturn app;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Extensions/AuthorizationOptionsExtensions.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Extensions;\n\npublic static class AuthorizationOptionsExtensions\n{\n\t/// <summary>\n\t/// Adds authorization options to require certain claims for moderator and admin roles in POP Forums.\n\t/// </summary>\n\t/// <param name=\"options\"></param>\n\tpublic static void AddPopForumsPolicies(this AuthorizationOptions options)\n\t{\n\t\toptions.AddPolicy(PermanentRoles.Admin, policy => policy.RequireClaim(PopForumsAuthenticationDefaults.ForumsClaimType, PermanentRoles.Admin));\n\t\toptions.AddPolicy(PermanentRoles.Moderator, policy => policy.RequireClaim(PopForumsAuthenticationDefaults.ForumsClaimType, PermanentRoles.Moderator));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Extensions/Logger.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Extensions;\n\npublic class Logger : ILogger\n{\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IHttpContextAccessor _httpContextAccessor;\n\n\tpublic Logger(IErrorLog errorLog, ISettingsManager settingsManager, IHttpContextAccessor httpContextAccessor)\n\t{\n\t\t_errorLog = errorLog;\n\t\t_settingsManager = settingsManager;\n\t\t_httpContextAccessor = httpContextAccessor;\n\t}\n\n\tpublic void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)\n\t{\n\t\tif (exception is ErrorLogException)\n\t\t\treturn;\n\t\tif (IsEnabled(logLevel))\n\t\t{\n\t\t\tstring data = null;\n\t\t\tif (_httpContextAccessor?.HttpContext == null)\n\t\t\t\tdata = \"HttpContext not available\";\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar context = _httpContextAccessor.HttpContext;\n\t\t\t\tif (context != null)\n\t\t\t\t{\n\t\t\t\t\tvar s = new StringBuilder();\n\t\t\t\t\tvar user =\n\t\t\t\t\t\tcontext.User.Identities.SingleOrDefault(\n\t\t\t\t\t\t\tx => x.AuthenticationType == PopForumsAuthenticationDefaults.AuthenticationScheme);\n\t\t\t\t\tif (user != null)\n\t\t\t\t\t{\n\t\t\t\t\t\ts.Append(\"User: \");\n\t\t\t\t\t\ts.Append(user.Name);\n\t\t\t\t\t\ts.Append(\"\\r\\n\");\n\t\t\t\t\t}\n\t\t\t\t\ts.Append(\"IP: \");\n\t\t\t\t\ts.Append(context.Connection.RemoteIpAddress);\n\t\t\t\t\ts.Append(\"\\r\\n\\r\\n\");\n\t\t\t\t\tforeach (var item in context.Request.Headers)\n\t\t\t\t\t{\n\t\t\t\t\t\ts.Append(item.Key);\n\t\t\t\t\t\ts.Append(\": \");\n\t\t\t\t\t\ts.Append(item.Value);\n\t\t\t\t\t\ts.Append(\"\\r\\n\");\n\t\t\t\t\t}\n\t\t\t\t\tdata = s.ToString();\n\t\t\t\t}\n\t\t\t}\n\t\t\t_errorLog.Log(exception, ErrorSeverity.Error, data);\n\t\t}\n\t}\n\n\tpublic bool IsEnabled(LogLevel logLevel)\n\t{\n\t\treturn logLevel == LogLevel.Error && _settingsManager.Current.LogErrors;\n\t}\n\n\tpublic IDisposable BeginScope<TState>(TState state)\n\t{\n\t\treturn NoopDisposable.Instance;\n\t}\n\n\tprivate class NoopDisposable : IDisposable\n\t{\n\t\tpublic static NoopDisposable Instance = new NoopDisposable();\n\n\t\tpublic void Dispose()\n\t\t{\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Extensions/LoggerFactories.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Extensions;\n\npublic static class LoggerFactories\n{\n\tpublic static ILoggerFactory AddPopForumsLogger(this ILoggerFactory logger, IApplicationBuilder app)\n\t{\n\t\tvar setupService = app.ApplicationServices.GetService<ISetupService>();\n\t\tif (!setupService.IsConnectionPossible() || !setupService.IsDatabaseSetup())\n\t\t\treturn logger;\n\t\tvar errorLog = app.ApplicationServices.GetService<IErrorLog>();\n\t\tvar settingsManager = app.ApplicationServices.GetService<ISettingsManager>();\n\t\tvar contextAccessor = app.ApplicationServices.GetService<IHttpContextAccessor>();\n\t\tlogger.AddProvider(new LoggerProvider(errorLog, settingsManager, contextAccessor));\n\t\treturn logger;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Extensions/LoggerProvider.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Extensions;\n\npublic class LoggerProvider : ILoggerProvider\n{\n\tprivate readonly IErrorLog _errorLog;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly IHttpContextAccessor _httpContextAccessor;\n\n\tpublic LoggerProvider(IErrorLog errorLog, ISettingsManager settingsManager, IHttpContextAccessor httpContextAccessor)\n\t{\n\t\t_errorLog = errorLog;\n\t\t_settingsManager = settingsManager;\n\t\t_httpContextAccessor = httpContextAccessor;\n\t}\n\n\tpublic void Dispose()\n\t{\n\t}\n\n\tpublic ILogger CreateLogger(string categoryName)\n\t{\n\t\treturn new Logger(_errorLog, _settingsManager, _httpContextAccessor);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Extensions/ServiceCollections.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\nusing PopForums.Mvc.Areas.Forums.BackgroundJobs;\nusing PopIdentity.Extensions;\n\nnamespace PopForums.Mvc.Areas.Forums.Extensions;\n\npublic static class ServiceCollections\n{\n\t/// <summary>\n\t/// Adds web project services to dependency injection container and authentication for POP Forums. This method \n\t/// fails if the ISetupService can't connect to the database or the database isn't set up.\n\t/// </summary>\n\t/// <param name=\"services\"></param>\n\t/// <returns>The updated IServiceCollection.</returns>\n\tpublic static IServiceCollection AddPopForumsMvc(this IServiceCollection services)\n\t{\n\t\treturn services.AddPopForumsMvc(true);\n\t}\n\n\t/// <summary>\n\t/// Adds web project services to dependency injection container and authentication for POP Forums. This method \n\t/// fails if the ISetupService can't connect to the database or the database isn't set up.\n\t/// </summary>\n\t/// <param name=\"services\"></param>\n\t/// <param name=\"includePopForumsBaseServices\">Indicate false if you intend to call\n\t/// services.AddPopForumsBase() on your own.</param>\n\t/// <returns>The updated IServiceCollection.</returns>\n\tpublic static IServiceCollection AddPopForumsMvc(this IServiceCollection services, bool includePopForumsBaseServices)\n\t{\n\t\tif (includePopForumsBaseServices)\n\t\t\tservices.AddPopForumsBase();\n\t\tservices.AddHttpContextAccessor();\n\t\tservices.AddPopIdentity();\n\t\tservices.AddTransient<IUserRetrievalShim, UserRetrievalShim>();\n\t\tservices.AddTransient<ITopicViewCountService, TopicViewCountService>();\n\t\tservices.AddTransient<IExternalLoginRoutingService, ExternalLoginRoutingService>();\n\t\tservices.AddTransient<IExternalLoginTempService, ExternalLoginTempService>();\n\t\tservices.AddTransient<IUserStateComposer, UserStateComposer>();\n\t\tservices.AddTransient<IBroker, Broker>();\n\t\tservices.AddTransient<IUserIdProvider, PopForumsUserIdProvider>();\n\t\tservices.AddTransient<IOAuthOnlyService, OAuthOnlyService>();\n\t\t\t\n\t\tvar serviceProvider = services.BuildServiceProvider();\n\t\tvar setupService = serviceProvider.GetService<ISetupService>();\n\t\tif (!setupService.IsConnectionPossible() || !setupService.IsDatabaseSetup())\n\t\t\treturn services;\n\n\t\tservices.AddAuthentication()\n\t\t\t.AddCookie(PopForumsAuthenticationDefaults.AuthenticationScheme, option =>\n\t\t\t{\n\t\t\t\toption.ExpireTimeSpan = new TimeSpan(365, 0, 0, 0);\n\t\t\t\toption.LoginPath = \"/Forums/Account/Login\";\n\t\t\t\t// TODO: This is lame because of fx, see: https://github.com/dotnet/aspnetcore/issues/9039\n\t\t\t\toption.Events.OnRedirectToAccessDenied = context =>\n\t\t\t\t{\n\t\t\t\t\tcontext.Response.StatusCode = 403;\n\t\t\t\t\treturn Task.CompletedTask;\n\t\t\t\t};\n\t\t\t});\n\n\t\treturn services;\n\t}\n\n\tpublic static IServiceCollection AddPopForumsBackgroundJobs(this IServiceCollection services)\n\t{\n\t\tservices.AddHostedService<EmailJob>();\n\t\tservices.AddHostedService<UserSessionJob>();\n\t\tservices.AddHostedService<SearchIndexJob>();\n\t\tservices.AddHostedService<AwardCalculatorJob>();\n\t\tservices.AddHostedService<CloseAgedTopicsJob>();\n\t\tservices.AddHostedService<PostImageCleanupJob>();\n\t\tservices.AddHostedService<SubscribeNotificationJob>();\n\t\treturn services;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Extensions/WebApplications.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Extensions;\n\npublic static class WebApplications\n{\n\t/// <summary>\n\t/// Adds the POP Forums app to the application.\n\t/// </summary>\n\t/// <param name=\"app\"></param>\n\t/// <returns></returns>\n\tpublic static WebApplication AddPopForumsEndpoints(this WebApplication app)\n\t{\n\t\tapp.MapHub<PopForumsHub>(\"/PopForumsHub\", options =>\n\t\t{\n\t\t\toptions.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;\n\t\t});\n\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfadmin\",\n\t\t\t\"Forums/Admin/{**vue}\",\n\t\t\tnew { controller = AdminController.Name, action = \"App\", Area = \"Forums\" });\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfadmin2\",\n\t\t\t\"Forums/Admin/{vue}/{**admin}\",\n\t\t\tnew { controller = AdminController.Name, action = \"App\", Area = \"Forums\" });\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfsetup\",\n\t\t\t\"Forums/Setup\",\n\t\t\tnew { controller = \"Setup\", action = \"Index\", Area = \"Forums\" }\n\t\t);\n\n\t\tvar setupService = app.Services.GetService<ISetupService>();\n\t\tif (!setupService.IsConnectionPossible() || !setupService.IsDatabaseSetup())\n\t\t\treturn app;\n\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfrecent1\",\n\t\t\t\"Forums/Recent\",\n\t\t\tnew { controller = ForumController.Name, action = \"Recent\", pageNumber = 1, Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfrecent\",\n\t\t\t\"Forums/Recent/{pageNumber}\",\n\t\t\tnew { controller = ForumController.Name, action = \"Recent\", Area = \"Forums\" }\n\t\t);\n\t\tvar forumRepository = app.Services.GetService<IForumRepository>();\n\t\tvar forumConstraint = new ForumRouteConstraint(forumRepository);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfroot1\",\n\t\t\t\"Forums/{urlName}\",\n\t\t\tnew { controller = ForumController.Name, action = \"Index\", pageNumber = 1, Area = \"Forums\" },\n\t\t\tnew { forum = forumConstraint }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfroot\",\n\t\t\t\"Forums/{urlName}/{pageNumber}\",\n\t\t\tnew { controller = ForumController.Name, action = \"Index\", Area = \"Forums\" },\n\t\t\tnew { forum = forumConstraint }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pftopic1\",\n\t\t\t\"Forums/Topic/{id}\",\n\t\t\tnew { controller = ForumController.Name, action = \"Topic\", pageNumber = 1, Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pftopic\",\n\t\t\t\"Forums/Topic/{id}/{pageNumber}\",\n\t\t\tnew { controller = ForumController.Name, action = \"Topic\", Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pflink\",\n\t\t\t\"Forums/PostLink/{id}\",\n\t\t\tnew { controller = ForumController.Name, action = \"PostLink\", Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfsubtopics1\",\n\t\t\t\"Forums/Subscription/Topics\",\n\t\t\tnew { controller = SubscriptionController.Name, action = \"Topics\", pageNumber = 1, Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfsubtopics\",\n\t\t\t\"Forums/Subscription/Topics/{pageNumber}\",\n\t\t\tnew { controller = SubscriptionController.Name, action = \"Topics\", Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pffavetopics1\",\n\t\t\t\"Forums/Favorites/Topics\",\n\t\t\tnew { controller = FavoritesController.Name, action = \"Topics\", pageNumber = 1, Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pffavetopics\",\n\t\t\t\"Forums/Favorites/Topics/{pageNumber}\",\n\t\t\tnew { controller = FavoritesController.Name, action = \"Topics\", Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfpagedusertopics1\",\n\t\t\t\"Forums/Account/Posts/{id}\",\n\t\t\tnew { controller = AccountController.Name, action = \"Posts\", pageNumber = 1, Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pfpagedusertopics\",\n\t\t\t\"Forums/Account/Posts/{id}/{pageNumber}\",\n\t\t\tnew { controller = AccountController.Name, action = \"Posts\", Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n\t\t\t\"pftopicunsub\",\n\t\t\t\"Forums/Subscription/Unsubscribe/{topicID}/{authKey}\",\n\t\t\tnew { controller = SubscriptionController.Name, action = \"Unsubscribe\", Area = \"Forums\" }\n\t\t);\n\t\tapp.MapControllerRoute(\n          \"pfignore\",\n          \"Forums/Ignore/{action}\",\n          new { controller = IgnoreController.Name, Area = \"Forums\" }\n        );\n\t\treturn app;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/ForumRouteConstraint.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums;\n\npublic class ForumRouteConstraint : IRouteConstraint\n{\n\tpublic ForumRouteConstraint(IForumRepository forumRepository)\n\t{\n\t\t_forumRepository = forumRepository;\n\t}\n\n\tprivate readonly IForumRepository _forumRepository;\n\n\tpublic bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values,\n\t\tRouteDirection routeDirection)\n\t{\n\t\tif (!values.Keys.Contains(\"urlName\"))\n\t\t\treturn false;\n\t\tIEnumerable<String> forumUrlNames;\n\t\ttry\n\t\t{\n\t\t\tforumUrlNames = _forumRepository.GetAllForumUrlNames().Result;\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\tthrow new Exception(\"Can't read forum URL names from data store.\", exc);\n\t\t}\n\t\tif (forumUrlNames.Contains(values[\"urlName\"]))\n\t\t\treturn true;\n\t\treturn false;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Messaging/Broker.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Messaging;\n\npublic class Broker : IBroker\n{\n\tpublic Broker(IForumRepository forumRepo, ITenantService tenantService, IHubContext<PopForumsHub> popForumsHubContext)\n\t{\n\t\t_forumRepo = forumRepo;\n\t\t_tenantService = tenantService;\n\t\t_popForumsHubContext = popForumsHubContext;\n\t}\n\t\n\tprivate readonly IForumRepository _forumRepo;\n\tprivate readonly ITenantService _tenantService;\n\tprivate readonly IHubContext<PopForumsHub> _popForumsHubContext;\n\n\tpublic void NotifyNewPosts(Topic topic, int lasPostID)\n\t{\n\t\tvar tenant = _tenantService.GetTenant();\n\t\t_popForumsHubContext.Clients.Group($\"{tenant}:topic:{topic.TopicID}\").SendAsync(\"notifyNewPosts\", lasPostID);\n\t}\n\n\tpublic void NotifyNewPost(Topic topic, int postID)\n\t{\n\t\tvar tenant = _tenantService.GetTenant();\n\t\t_popForumsHubContext.Clients.Group($\"{tenant}:topic:{topic.TopicID}\").SendAsync(\"fetchNewPost\", postID);\n\t}\n\n\tpublic void NotifyForumUpdate(Forum forum)\n\t{\n\t\tvar tenant = _tenantService.GetTenant();\n\t\t_popForumsHubContext.Clients.Group($\"{tenant}:forum:all\").SendAsync(\"notifyForumUpdate\", new { forum.ForumID, TopicCount = forum.TopicCount.ToString(\"N0\"), PostCount = forum.PostCount.ToString(\"N0\"), forum.LastPostName, Utc = forum.LastPostTime.ToString(\"o\") });\n\t}\n\n\tpublic void NotifyTopicUpdate(Topic topic, Forum forum, string topicLink)\n\t{\n\t\tvar tenant = _tenantService.GetTenant();\n\t\tvar isForumViewRestricted = _forumRepo.GetForumViewRoles(forum.ForumID).Result.Count > 0;\n\t\tvar result = new\n\t\t{\n\t\t\tLink = topicLink,\n\t\t\ttopic.TopicID,\n\t\t\ttopic.StartedByName,\n\t\t\ttopic.Title,\n\t\t\tForumTitle = forum.Title,\n\t\t\ttopic.ViewCount,\n\t\t\ttopic.ReplyCount,\n\t\t\tUtc = topic.LastPostTime.ToString(\"o\"),\n\t\t\ttopic.LastPostName\n\t\t};\n\t\tif (isForumViewRestricted)\n\t\t\t_popForumsHubContext.Clients.Group($\"{tenant}:recent:{forum.ForumID}\").SendAsync(\"notifyRecentUpdate\", result);\n\t\telse\n\t\t\t_popForumsHubContext.Clients.Group($\"{tenant}:recent:all\").SendAsync(\"notifyRecentUpdate\", result);\n\t\t_popForumsHubContext.Clients.Group($\"{tenant}:forum:{forum.ForumID}\").SendAsync(\"notifyUpdatedTopic\", result);\n\t}\n\n\tpublic async void NotifyPMCount(int userID, int pmCount)\n\t{\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tvar userIDString = PopForumsUserIdProvider.FormatUserID(tenantID, userID);\n\t\tawait _popForumsHubContext.Clients.User(userIDString).SendAsync(\"updatePMCount\", pmCount);\n\t}\n\n\tpublic async void NotifyUser(Notification notification)\n\t{\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tvar userIDString = PopForumsUserIdProvider.FormatUserID(tenantID, notification.UserID);\n\t\tawait _popForumsHubContext.Clients.User(userIDString).SendAsync(\"notify\", notification);\n\t}\n\n\tpublic async void NotifyUser(Notification notification, string tenantID)\n\t{\n\t\tvar userIDString = PopForumsUserIdProvider.FormatUserID(tenantID, notification.UserID);\n\t\tawait _popForumsHubContext.Clients.User(userIDString).SendAsync(\"notify\", notification);\n\t}\n\n\tpublic async void SendPMMessage(PrivateMessagePost post)\n\t{\n\t\tvar message = ClientPrivateMessagePost.MapForClient(post);\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tawait _popForumsHubContext.Clients.Group($\"{tenantID}:pm:{post.PMID}\").SendAsync(\"addMessage\", message);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Messaging/PopForumsHub.cs",
    "content": "﻿using PopForums.Mvc.Areas.Forums.Authentication;\n\nnamespace PopForums.Mvc.Areas.Forums.Messaging;\n\npublic class PopForumsHub : Hub\n{\n\tprivate readonly ITenantService _tenantService;\n\tprivate readonly IUserService _userService;\n\tprivate readonly IForumService _forumService;\n\tprivate readonly ITopicService _topicService;\n\tprivate readonly INotificationManager _notificationManager;\n\tprivate readonly IPrivateMessageService _privateMessageService;\n\n\tpublic PopForumsHub(ITenantService tenantService, IUserService userService, IForumService forumService, ITopicService topicService, INotificationManager notificationManager, IPrivateMessageService privateMessageService)\n\t{\n\t\t_tenantService = tenantService;\n\t\t_userService = userService;\n\t\t_forumService = forumService;\n\t\t_topicService = topicService;\n\t\t_notificationManager = notificationManager;\n\t\t_privateMessageService = privateMessageService;\n\t}\n\n\t// *** Forums\n\n\tpublic void ListenToAllForums()\n\t{\n\t\tvar tenant = _tenantService.GetTenant();\n\t\tGroups.AddToGroupAsync(Context.ConnectionId, $\"{tenant}:forum:all\");\n\t}\n\n\tpublic void ListenToForum(int forumID)\n\t{\n\t\tvar tenant = _tenantService.GetTenant();\n\t\tGroups.AddToGroupAsync(Context.ConnectionId, $\"{tenant}:forum:{forumID}\");\n\t}\n\n\t// *** Recent\n\n\tpublic void ListenRecent()\n\t{\n\t\tvar tenant = _tenantService.GetTenant();\n\t\tGroups.AddToGroupAsync(Context.ConnectionId, $\"{tenant}:recent:all\");\n\t\tvar principal = Context.User;\n\t\tif (principal != null && principal.Identity != null)\n\t\t{\n\t\t\tvar user = _userService.GetUserByName(principal.Identity.Name).Result;\n\t\t\tvar visibleForumIDs = _forumService.GetViewableForumIDsFromViewRestrictedForums(user).Result;\n\t\t\tforeach (var forumID in visibleForumIDs)\n\t\t\t\tGroups.AddToGroupAsync(Context.ConnectionId, $\"{tenant}:recent:{forumID}\");\n\t\t}\n\t}\n\n\t// *** Topics\n\n\tpublic void ListenToTopic(int topicID)\n\t{\n\t\tvar tenant = _tenantService.GetTenant();\n\t\tGroups.AddToGroupAsync(Context.ConnectionId, $\"{tenant}:topic:{topicID}\");\n\t}\n\n\tpublic int GetLastPostID(int topicID)\n\t{\n\t\treturn _topicService.TopicLastPostID(topicID).Result;\n\t}\n\n\t// *** Notifications\n\n\tprivate int GetUserID()\n\t{\n\t\tvar userIDstring = Context.User?.Claims.FirstOrDefault(x => x.Type == PopForumsAuthenticationDefaults.ForumsUserIDType);\n\t\tif (userIDstring == null)\n\t\t\tthrow new Exception(\"No forum user ID claim found in hub context of User\");\n\t\tvar userID = Convert.ToInt32(userIDstring.Value);\n\t\treturn userID;\n\t}\n\n\tpublic async Task MarkNotificationRead(long contextID, int notificationType)\n\t{\n\t\tvar notificationTypeEnum = (NotificationType)notificationType;\n\t\tvar userID = GetUserID();\n\t\tawait _notificationManager.MarkNotificationRead(userID, notificationTypeEnum, contextID);\n\t}\n\n\tpublic async Task MarkAllRead()\n\t{\n\t\tvar userID = GetUserID();\n\t\tawait _notificationManager.MarkAllRead(userID);\n\t}\n\n\tpublic async Task<List<Notification>> GetNotifications(DateTime afterDateTime)\n\t{\n\t\tvar userID = GetUserID();\n\t\tvar notifications = await _notificationManager.GetNotifications(userID, afterDateTime);\n\t\treturn notifications;\n\t}\n\n\tpublic async Task<int> GetNotificationCount()\n\t{\n\t\tvar userID = GetUserID();\n\t\tvar count = await _notificationManager.GetUnreadNotificationCount(userID);\n\t\treturn count;\n\t}\n\n\tpublic async Task<int> GetPMCount()\n\t{\n\t\tvar userID = GetUserID();\n\t\tvar count = await _privateMessageService.GetUnreadCount(userID);\n\t\treturn count;\n\t}\n\n\t// *** Private Messages\n\n\tpublic async Task ListenToPm(int pmID)\n\t{\n\t\tvar userID = GetUserID();\n\t\tif (!await _privateMessageService.IsUserInPM(userID, pmID))\n\t\t\treturn;\n\t\tvar tenant = _tenantService.GetTenant();\n\t\tawait Groups.AddToGroupAsync(Context.ConnectionId, $\"{tenant}:pm:{pmID}\");\n\t}\n\n\tpublic async Task SendPm(int pmID, string fullText)\n\t{\n\t\tvar userID = GetUserID();\n\t\tif (!await _privateMessageService.IsUserInPM(userID, pmID))\n\t\t\treturn;\n\t\tvar pm = await _privateMessageService.Get(pmID, userID);\n\t\tvar user = await _userService.GetUser(userID);\n\t\tawait _privateMessageService.Reply(pm, fullText, user);\n\t}\n\n\tpublic async Task AckReadPm(int pmID)\n\t{\n\t\tvar userID = GetUserID();\n\t\tawait _privateMessageService.MarkPMRead(userID, pmID);\n\t}\n\n\tpublic async Task<ClientPrivateMessagePost[]> GetPmPosts(int pmID, DateTime beforeDateTime)\n\t{\n\t\tvar userID = GetUserID();\n\t\tif (!await _privateMessageService.IsUserInPM(userID, pmID))\n\t\t{\n\t\t\treturn null;\n\t\t}\n\t\tvar posts = await _privateMessageService.GetPosts(pmID, beforeDateTime);\n\t\tvar clientMessages = ClientPrivateMessagePost.MapForClient(posts);\n\t\treturn clientMessages;\n\t}\n\n\tpublic async Task<ClientPrivateMessagePost[]> GetMostRecentPmPosts(int pmID, DateTime afterDateTime)\n\t{\n\t\tvar userID = GetUserID();\n\t\tif (!await _privateMessageService.IsUserInPM(userID, pmID))\n\t\t{\n\t\t\treturn null;\n\t\t}\n\t\tvar posts = await _privateMessageService.GetMostRecentPosts(pmID, afterDateTime);\n\t\tvar clientMessages = ClientPrivateMessagePost.MapForClient(posts);\n\t\treturn clientMessages;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Messaging/PopForumsUserIdProvider.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Messaging;\n\npublic class PopForumsUserIdProvider : IUserIdProvider\n{\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly ITenantService _tenantService;\n\n\tpublic PopForumsUserIdProvider(IUserRetrievalShim userRetrievalShim, ITenantService tenantService)\n\t{\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_tenantService = tenantService;\n\t}\n\n\tpublic string GetUserId(HubConnectionContext connection)\n\t{\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user == null)\n\t\t\treturn null;\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tvar id = FormatUserID(tenantID, user.UserID);\n\t\treturn id;\n\t}\n\n\tpublic static string FormatUserID(string tenantID, int userID)\n\t{\n\t\treturn $\"{tenantID}:{userID}\";\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/AwardConditionDeleteContainer.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class AwardConditionDeleteContainer\n{\n\tpublic string AwardDefinitionID { get; set; }\n\tpublic string EventDefinitionID { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/EmailUsersContainer.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class EmailUsersContainer\n{\n\tpublic string Subject { get; set; }\n\tpublic string Body { get; set; }\n\tpublic string HtmlBody { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/ExternalLoginState.cs",
    "content": "﻿using PopIdentity;\n\nnamespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class ExternalLoginState\n{\n\tpublic ResultData ResultData { get; set; }\n\tpublic string ReturnUrl { get; set; }\n\tpublic ProviderType ProviderType { get; set; }\n\tpublic DateTime? Expiration { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/ExternalLoginTypeMetadata.cs",
    "content": "﻿using PopIdentity;\n\nnamespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class ExternalLoginTypeMetadata\n{\n\tpublic string Name { get; set; }\n\tpublic string CssClass { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/IPHistoryQuery.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class IPHistoryQuery\n{\n\tpublic string IP { get; set; }\n\tpublic DateTime Start { get; set; }\n\tpublic DateTime End { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/ManualEvent.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class ManualEvent\n{\n\tpublic int UserID { get; set; }\n\tpublic string Message { get; set; }\n\tpublic int? Points { get; set; }\n\tpublic string EventDefinitionID { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/SecurityLogQuery.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class SecurityLogQuery\n{\n\tpublic string SearchTerm { get; set; }\n\tpublic string Type { get; set; }\n\tpublic DateTime Start { get; set; }\n\tpublic DateTime End { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/UserEditPhoto.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class UserEditPhoto\n{\n\tpublic UserEditPhoto() { }\n\n\tpublic UserEditPhoto(Profile profile)\n\t{\n\t\tAvatarID = profile.AvatarID;\n\t\tImageID = profile.ImageID;\n\t}\n\n\tpublic int? AvatarID { get; set; }\n\tpublic int? ImageID { get; set; }\n\tpublic bool? IsImageApproved { get; set; }\n\tpublic bool DeleteAvatar { get; set; }\n\tpublic bool DeleteImage { get; set; }\n\tpublic IFormFile AvatarFile { get; set; }\n\tpublic IFormFile PhotoFile { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/UserEditWithFiles.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class UserEditWithFiles : UserEdit\n{\n\tpublic UserEditWithFiles()\n\t{\n\t\t    \n\t}\n\n\tpublic UserEditWithFiles(User user, Profile profile) : base(user, profile)\n\t{\n\t}\n\n\tpublic IFormFile AvatarFile { get; set; }\n\tpublic IFormFile PhotoFile { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Models/UserState.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Models;\n\npublic class UserState\n{\n\tpublic bool IsImageEnabled { get; set; }\n\tpublic bool IsPlainText { get; set; }\n\tpublic int NewPmCount { get; set; }\n\tpublic int NotificationCount { get; set; }\n\tpublic int UserID { get; set; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Services/ExternalLoginRoutingService.cs",
    "content": "﻿using PopIdentity;\n\nnamespace PopForums.Mvc.Areas.Forums.Services;\n\npublic interface IExternalLoginRoutingService\n{\n\tDictionary<ProviderType, ExternalLoginTypeMetadata> GetActiveProviderTypeAndNameDictionary();\n}\n\npublic class ExternalLoginRoutingService : IExternalLoginRoutingService\n{\n\tprivate readonly ISettingsManager _settingsManager;\n\n\tpublic ExternalLoginRoutingService(ISettingsManager settingsManager)\n\t{\n\t\t_settingsManager = settingsManager;\n\t}\n\n\tpublic Dictionary<ProviderType, ExternalLoginTypeMetadata> GetActiveProviderTypeAndNameDictionary()\n\t{\n\t\tvar dictionary = new Dictionary<ProviderType, ExternalLoginTypeMetadata>();\n\t\tif (_settingsManager.Current.UseGoogleLogin)\n\t\t\tdictionary.Add(ProviderType.Google, new ExternalLoginTypeMetadata { Name = \"Google\", CssClass = \"icon-google\" });\n\t\tif (_settingsManager.Current.UseFacebookLogin)\n\t\t\tdictionary.Add(ProviderType.Facebook, new ExternalLoginTypeMetadata { Name = \"Facebook\", CssClass = \"icon-facebook\" });\n\t\tif (_settingsManager.Current.UseMicrosoftLogin)\n\t\t\tdictionary.Add(ProviderType.Microsoft, new ExternalLoginTypeMetadata { Name = \"Microsoft\", CssClass = \"icon-microsoft\" });\n\t\tif (_settingsManager.Current.UseOAuth2Login)\n\t\t\tdictionary.Add(ProviderType.OAuth2, new ExternalLoginTypeMetadata { Name = _settingsManager.Current.OAuth2DisplayName, CssClass = \"\" });\n\t\treturn dictionary;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Services/ExternalLoginTempService.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Services;\n\npublic interface IExternalLoginTempService\n{\n\tvoid Persist(ExternalLoginState externalLoginState);\n\tExternalLoginState Read();\n\tvoid Remove();\n}\n\npublic class ExternalLoginTempService : IExternalLoginTempService\n{\n\tprivate readonly IDataProtectionProvider _dataProtectionProvider;\n\tprivate readonly IHttpContextAccessor _httpContextAccessor;\n\tprivate const string CookieKey = \"pf_temploginstate\";\n\n\tpublic ExternalLoginTempService(IDataProtectionProvider dataProtectionProvider, IHttpContextAccessor httpContextAccessor)\n\t{\n\t\t_dataProtectionProvider = dataProtectionProvider;\n\t\t_httpContextAccessor = httpContextAccessor;\n\t}\n\n\tpublic void Persist(ExternalLoginState externalLoginState)\n\t{\n\t\tvar protector = _dataProtectionProvider.CreateProtector(nameof(ExternalLoginTempService));\n\t\tvar serializedResult = JsonSerializer.Serialize(externalLoginState);\n\t\tvar encryptedResult = protector.Protect(serializedResult);\n\t\t_httpContextAccessor.HttpContext.Response.Cookies.Append(CookieKey, encryptedResult);\n\t}\n\n\tpublic ExternalLoginState Read()\n\t{\n\t\tvar protector = _dataProtectionProvider.CreateProtector(nameof(ExternalLoginTempService));\n\t\tvar encryptedTempAuth = _httpContextAccessor.HttpContext.Request.Cookies[CookieKey];\n\t\tif (string.IsNullOrEmpty(encryptedTempAuth))\n\t\t{\n\t\t\treturn null;\n\t\t}\n\t\tvar decryptedSerialized = protector.Unprotect(encryptedTempAuth);\n\t\tvar result = JsonSerializer.Deserialize<ExternalLoginState>(decryptedSerialized);\n\t\treturn result;\n\t}\n\n\tpublic void Remove()\n\t{\n\t\t_httpContextAccessor.HttpContext.Response.Cookies.Delete(CookieKey);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Services/ForumAdapterFactory.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Services;\n\npublic class ForumAdapterFactory\n{\n\tpublic ForumAdapterFactory(Forum forum)\n\t{\n\t\tif (!string.IsNullOrWhiteSpace(forum.ForumAdapterName))\n\t\t{\n\t\t\tvar type = Type.GetType(forum.ForumAdapterName);\n\t\t\tif (type == null)\n\t\t\t\tthrow new Exception($\"Can't find ForumAdapter \\\"{forum.ForumAdapterName}\\\" (Forum ID: {forum.ForumID}, Title: {forum.Title})\");\n\t\t\tvar instance = Activator.CreateInstance(type);\n\t\t\tif (!typeof(IForumAdapter).IsAssignableFrom(instance.GetType()))\n\t\t\t\tthrow new Exception($\"ForumAdapter \\\"{forum.ForumAdapterName}\\\" does not implement IForumAdapter (Forum ID: {forum.ForumID}, Title: {forum.Title})\");\n\t\t\tForumAdapter = (IForumAdapter)instance;\n\t\t}\n\t}\n\n\tpublic bool IsAdapterEnabled => ForumAdapter != null;\n\tpublic IForumAdapter ForumAdapter { get; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Services/IForumAdapter.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Services;\n\n/// <summary>\n/// Enables alternate views and processing to individual forums.\n/// </summary>\n/// <remarks>\n/// Concrete implementations of this interface can be specified in the admin forum properties. The\n/// Index, Topic and PostLink methods of the forum controller will alter their behavior based on \n/// the implementation. See individual members for details.\n/// </remarks>\npublic interface IForumAdapter\n{\n\t/// <summary>\n\t/// This method allows you to change the view and model generated by the Index method of the ForumController by setting\n\t/// the properties on the adapter.\n\t/// </summary>\n\t/// <param name=\"controller\">Instance of the controller calling the adapter.</param>\n\t/// <param name=\"forumTopicContainer\">The composed ForumTopicContainer created by the Index method of the ForumController.</param>\n\tTask AdaptForum(Controller controller, ForumTopicContainer forumTopicContainer);\n\n\t/// <summary>\n\t/// This method allows you to change the view and model generated by the Topic method of the ForumController by setting\n\t/// the properties on the adapter.\n\t/// </summary>\n\t/// <param name=\"controller\">Instance of the controller calling the adapter.</param>\n\t/// <param name=\"topicContainer\">The composed TopicContainer created by the Topic method of the ForumController.</param>\n\tTask AdaptTopic(Controller controller, TopicContainer topicContainer);\n\n\t/// <summary>\n\t/// This method allows you to override the behavior of the ForumController's PostLink \n\t/// method. Use this if you need the links to redirect to some other location, otherwise\n\t/// return null.\n\t/// </summary>\n\t/// <param name=\"controller\">Instance of the controller calling the adapter.</param>\n\t/// <param name=\"post\">The post associated with the id parameter of the PostLink method.</param>\n\t/// <param name=\"topic\">The topic associated with the post.</param>\n\t/// <param name=\"forum\">The forum associated with the topic.</param>\n\t/// <returns></returns>\n\tTask<RedirectResult> AdaptPostLink(Controller controller, Post post, Topic topic, Forum forum);\n\n\t/// <summary>\n\t/// Set this in the adapter for alternate views from the AdaptForum and AdaptTopic methods.\n\t/// </summary>\n\tstring ViewName { get; }\n\n\t/// <summary>\n\t/// Set this in the adapter for alternate an alternate model from the AdaptForum and AdaptTopic methods.\n\t/// </summary>\n\tobject Model { get; }\n\n\t/// <summary>\n\t/// To allow the controller to mark the topic as read, set this to true, otherwise, false.\n\t/// </summary>\n\tbool MarkViewedTopicRead { get; }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Services/OAuthOnlyService.cs",
    "content": "using PopIdentity;\nusing PopIdentity.Providers.OAuth2;\n\nnamespace PopForums.Mvc.Areas.Forums.Services;\n\npublic interface IOAuthOnlyService\n{\n\tstring GetLoginUrl(string redirectUrl);\n\tTask<CallbackResult> ProcessOAuthLogin(string redirectUrl, string ip);\n\tTask<bool> AttemptTokenRefresh(User user);\n}\n\npublic class OAuthOnlyService : IOAuthOnlyService\n{\n\tprivate readonly IConfig _config;\n\tprivate readonly IOAuth2LoginUrlGenerator _oAuth2LoginUrlGenerator;\n\tprivate readonly IStateHashingService _stateHashingService;\n\tprivate readonly IOAuth2JwtCallbackProcessor _oAuth2JwtCallbackProcessor;\n\tprivate readonly IExternalUserAssociationManager _externalUserAssociationManager;\n\tprivate readonly IUserService _userService;\n\tprivate readonly IClaimsToRoleMapper _claimsToRoleMapper;\n\tprivate readonly IUserNameReconciler _userNameReconciler;\n\tprivate readonly IUserEmailReconciler _userEmailReconciler;\n\tprivate readonly ISecurityLogService _securityLogService;\n\n\tpublic OAuthOnlyService(IConfig config, IOAuth2LoginUrlGenerator oAuth2LoginUrlGenerator, IStateHashingService stateHashingService, IOAuth2JwtCallbackProcessor oAuth2JwtCallbackProcessor, IExternalUserAssociationManager externalUserAssociationManager, IUserService userService, IClaimsToRoleMapper claimsToRoleMapper, IUserNameReconciler userNameReconciler, IUserEmailReconciler userEmailReconciler, ISecurityLogService securityLogService)\n\t{\n\t\t_config = config;\n\t\t_oAuth2LoginUrlGenerator = oAuth2LoginUrlGenerator;\n\t\t_stateHashingService = stateHashingService;\n\t\t_oAuth2JwtCallbackProcessor = oAuth2JwtCallbackProcessor;\n\t\t_externalUserAssociationManager = externalUserAssociationManager;\n\t\t_userService = userService;\n\t\t_claimsToRoleMapper = claimsToRoleMapper;\n\t\t_userNameReconciler = userNameReconciler;\n\t\t_userEmailReconciler = userEmailReconciler;\n\t\t_securityLogService = securityLogService;\n\t}\n\n\tpublic string GetLoginUrl(string redirectUrl)\n\t{\n\t\tvar state = _stateHashingService.SetCookieAndReturnHash();\n\t\tvar url = _oAuth2LoginUrlGenerator.GetUrl(_config.OAuthLoginBaseUrl, _config.OAuthClientID, redirectUrl, state,\n\t\t\t_config.OAuthScopes);\n\t\treturn url;\n\t}\n\n\tpublic async Task<CallbackResult> ProcessOAuthLogin(string redirectUrl, string ip)\n\t{\n\t\tvar callbackResult = await _oAuth2JwtCallbackProcessor.VerifyCallback(redirectUrl, _config.OAuthTokenUrl,\n\t\t\t_config.OAuthClientID, _config.OAuthClientSecret);\n\t\tif (!callbackResult.IsSuccessful)\n\t\t{\n\t\t\tawait _securityLogService.CreateLogEntry((User)null, null, ip, callbackResult.Message, SecurityLogType.ExternalLoginChallengeFailed);\n\t\t\treturn callbackResult;\n\t\t}\n\t\tif (string.IsNullOrEmpty(callbackResult.ResultData.Name))\n\t\t{\n\t\t\tcallbackResult.IsSuccessful = false;\n\t\t\tcallbackResult.Message = \"Identity provider did not return a name.\";\n\t\t}\n\t\tif (string.IsNullOrEmpty(callbackResult.ResultData.Email))\n\t\t{\n\t\t\tcallbackResult.IsSuccessful = false;\n\t\t\tcallbackResult.Message = \"Identity provider did not return an email.\";\n\t\t}\n\t\tif (string.IsNullOrEmpty(callbackResult.ResultData.ID))\n\t\t{\n\t\t\tcallbackResult.IsSuccessful = false;\n\t\t\tcallbackResult.Message = \"Identity provider did not return a unique identifier.\";\n\t\t}\n\t\t\n\t\t// lookup the external user\n\t\tvar externalLoginInfo = new ExternalLoginInfo(\n\t\t\tProviderType.OAuthOnly.ToString(), \n\t\t\tcallbackResult.ResultData.ID, \n\t\t\tcallbackResult.ResultData.Name);\n\t\tvar matchResult = await _externalUserAssociationManager.ExternalUserAssociationCheck(externalLoginInfo, ip);\n\n\t\tUser user;\n\t\tif (!matchResult.Successful)\n\t\t{\n\t\t\t// if not found, create the new user\n\t\t\t// reconcile email\n\t\t\tvar uniqueEmail = await _userEmailReconciler.GetUniqueEmail(callbackResult.ResultData.Email, callbackResult.ResultData.ID);\n\t\t\t// reconcile name\n\t\t\tvar uniqueName = await _userNameReconciler.GetUniqueNameForUser(callbackResult.ResultData.Name);\n\t\t\tvar signupData = new SignupData\n\t\t\t{\n\t\t\t\tName = uniqueName,\n\t\t\t\tEmail = uniqueEmail,\n\t\t\t\tPassword = Guid.NewGuid().ToString(),\n\t\t\t\tIsCoppa = true,\n\t\t\t\tIsTos = true,\n\t\t\t\tIsSubscribed = true,\n\t\t\t\tIsAutoFollowOnReply = true\n\t\t\t};\n\t\t\tuser = await _userService.CreateUserWithProfile(signupData, ip);\n\t\t\tawait _externalUserAssociationManager.Associate(user, externalLoginInfo, ip);\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// if found, verify name is correct\n\t\t\tuser = matchResult.User;\n\t\t\t// reconcile name\n\t\t\tif (user.Name != callbackResult.ResultData.Name)\n\t\t\t{\n\t\t\t\tvar updatedName = await _userNameReconciler.GetUniqueNameForUser(callbackResult.ResultData.Name);\n\t\t\t\tawait _userService.ChangeName(user, updatedName, null, ip);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// set the token expiration\n\t\tawait _userService.UpdateTokenExpiration(user, DateTime.UtcNow.AddMinutes(_config.OAuthRefreshExpirationMinutes));\n\t\t// update refresh token\n\t\tawait _userService.UpdateRefreshToken(user, callbackResult.RefreshToken);\n\t\t\n\t\t// set admin/mod based on claims\n\t\tawait _claimsToRoleMapper.MapRoles(user, callbackResult.Claims);\n\n\t\treturn callbackResult;\n\t}\n\n\tpublic async Task<bool> AttemptTokenRefresh(User user)\n\t{\n\t\tvar previousToken = await _userService.GetRefreshToken(user);\n\t\tvar callbackResult = await _oAuth2JwtCallbackProcessor.GetRefreshToken(previousToken, _config.OAuthTokenUrl,\n\t\t\t_config.OAuthClientID, _config.OAuthClientSecret);\n\t\tif (callbackResult.IsSuccessful)\n\t\t{\n\t\t\tawait _userService.UpdateRefreshToken(user, callbackResult.RefreshToken);\n\t\t\tawait _userService.UpdateTokenExpiration(user, DateTime.UtcNow.AddMinutes(_config.OAuthRefreshExpirationMinutes));\n\t\t}\n\t\treturn callbackResult.IsSuccessful;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Services/TopicViewCountService.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Services;\n\npublic class TopicViewCountService : ITopicViewCountService\n{\n\tpublic TopicViewCountService(ITopicRepository topicRepository, IHttpContextAccessor httpContextAccessor)\n\t{\n\t\t_topicRepository = topicRepository;\n\t\t_httpContextAccessor = httpContextAccessor;\n\t}\n\n\tprivate readonly ITopicRepository _topicRepository;\n\tprivate readonly IHttpContextAccessor _httpContextAccessor;\n\tprivate const string CookieKey = \"PopForums.LastTopicID\";\n\n\tpublic async Task ProcessView(Topic topic)\n\t{\n\t\tvar context = _httpContextAccessor.HttpContext;\n\t\tif (context != null && context.Request.Cookies != null && context.Request.Cookies.ContainsKey(CookieKey))\n\t\t{\n\t\t\tif (int.TryParse(context.Request.Cookies[CookieKey], out var topicID))\n\t\t\t{\n\t\t\t\tif (topicID != topic.TopicID)\n\t\t\t\t\tawait _topicRepository.IncrementViewCount(topic.TopicID);\n\t\t\t}\n\t\t}\n\t\telse\n\t\t\tawait _topicRepository.IncrementViewCount(topic.TopicID);\n\t\tSetViewedTopic(topic);\n\t}\n\n\tpublic void SetViewedTopic(Topic topic)\n\t{\n\t\t_httpContextAccessor?.HttpContext?.Response?.Cookies?.Append(CookieKey, topic.TopicID.ToString());\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Services/UserRetrievalShim.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Services;\n\npublic class UserRetrievalShim : IUserRetrievalShim\n{\n\tprivate readonly IHttpContextAccessor _httpContextAccessor;\n\n\tpublic UserRetrievalShim(IHttpContextAccessor httpContextAccessor)\n\t{\n\t\t_httpContextAccessor = httpContextAccessor;\n\t}\n\n\tpublic User GetUser()\n\t{\n\t\tvar user = _httpContextAccessor.HttpContext?.Items[\"PopForumsUser\"] as User;\n\t\treturn user;\n\t}\n\n\tpublic Profile GetProfile()\n\t{\n\t\tvar profile = _httpContextAccessor.HttpContext?.Items[\"PopForumsProfile\"] as Profile;\n\t\treturn profile;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Services/UserStateComposer.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.Services;\n\npublic interface IUserStateComposer\n{\n\tTask<UserState> GetState();\n}\n\n// TODO: move this to base library\npublic class UserStateComposer : IUserStateComposer\n{\n\tprivate readonly IUserRetrievalShim _userRetrievalShim;\n\tprivate readonly IPrivateMessageService _privateMessageService;\n\tprivate readonly ISettingsManager _settingsManager;\n\tprivate readonly INotificationManager _notificationManager;\n\n\tpublic UserStateComposer(IUserRetrievalShim userRetrievalShim, IPrivateMessageService privateMessageService, ISettingsManager settingsManager, INotificationManager notificationManager)\n\t{\n\t\t_userRetrievalShim = userRetrievalShim;\n\t\t_privateMessageService = privateMessageService;\n\t\t_settingsManager = settingsManager;\n\t\t_notificationManager = notificationManager;\n\t}\n\n\tpublic async Task<UserState> GetState()\n\t{\n\t\tvar state = new UserState();\n\t\tvar user = _userRetrievalShim.GetUser();\n\t\tif (user != null)\n\t\t{\n\t\t\tvar profile = _userRetrievalShim.GetProfile();\n\t\t\tstate.IsPlainText = profile.IsPlainText;\n\t\t\tstate.NewPmCount = await _privateMessageService.GetUnreadCount(user.UserID);\n\t\t\tstate.IsImageEnabled = _settingsManager.Current.AllowImages;\n\t\t\tstate.NotificationCount = await _notificationManager.GetUnreadNotificationCount(user.UserID);\n\t\t\tstate.UserID = user.UserID;\n\t\t}\n\t\treturn state;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/TagHelpers/ForumReadIndicatorTagHelper.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.TagHelpers;\n\n[HtmlTargetElement(\"pf-forumReadIndicator\", Attributes = \"forum, categorizedForumContainer\")]\npublic class ForumReadIndicatorTagHelper : TagHelper\n{\n\t[HtmlAttributeName(\"forum\")]\n\tpublic Forum Forum { get; set; }\n\t[HtmlAttributeName(\"categorizedForumContainer\")]\n\tpublic CategorizedForumContainer CategorizedForumContainer { get; set; }\n\t[HtmlAttributeName(\"class\")]\n\tpublic string Class { get; set; }\n\n\tpublic override void Process(TagHelperContext context, TagHelperOutput output)\n\t{\n\t\tvar alt = Resources.NoNewPosts;\n\t\tif (CategorizedForumContainer.ReadStatusLookup.ContainsKey(Forum.ForumID))\n\t\t{\n\t\t\tvar status = CategorizedForumContainer.ReadStatusLookup[Forum.ForumID];\n\t\t\tswitch (status)\n\t\t\t{\n\t\t\t\tcase ReadStatus.Closed | ReadStatus.NoNewPosts:\n\t\t\t\t\talt = Resources.Archived;\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text text-muted\\\"></span><span class=\\\"icon icon-lock-fill firstBadge topicIndicatorBadge text-danger\\\"></span>\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.Closed | ReadStatus.NewPosts:\n\t\t\t\t\talt = Resources.ArchivedNewPosts;\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text-fill text-warning\\\"></span><span class=\\\"icon icon-lock-fill firstBadge topicIndicatorBadge text-danger\\\"></span>\");\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.NewPosts:\n\t\t\t\t\talt = Resources.NewPosts;\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text-fill text-warning\\\"></span>\");\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text text-muted\\\"></span>\");\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\toutput.TagName = \"div\";\n\t\toutput.Attributes.Add(\"title\", alt);\n\t\tif (!String.IsNullOrWhiteSpace(Class))\n\t\t\toutput.Attributes.Add(\"class\", $\"topicIndicator {Class}\");\n\t\telse\n\t\t\toutput.Attributes.Add(\"class\", \"topicIndicator\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/TagHelpers/PMReadIndicatorTagHelper.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.TagHelpers;\n\n[HtmlTargetElement(\"pf-pmReadIndicator\", Attributes = \"privateMessage\")]\npublic class PMReadIndicatorTagHelper : TagHelper\n{\n\t[HtmlAttributeName(\"class\")]\n\tpublic string Class { get; set; }\n\t[HtmlAttributeName(\"privateMessage\")]\n\tpublic PrivateMessage PrivateMessage { get; set; }\n\n\tpublic override void Process(TagHelperContext context, TagHelperOutput output)\n\t{\n\t\tif (PrivateMessage.LastPostTime > PrivateMessage.LastViewDate)\n\t\t{\n\t\t\toutput.Attributes.Add(\"title\", Resources.NewPosts);\n\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text-fill text-warning\\\"></span>\");\n\t\t}\n\t\telse\n\t\t{\n\t\t\toutput.Attributes.Add(\"title\", Resources.NoNewPosts);\n\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text text-muted\\\"></span>\");\n\t\t}\n\n\t\toutput.TagName = \"div\";\n\t\tif (!String.IsNullOrWhiteSpace(Class))\n\t\t\toutput.Attributes.Add(\"class\", $\"topicIndicator {Class}\");\n\t\telse\n\t\t\toutput.Attributes.Add(\"class\", \"topicIndicator\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/TagHelpers/PagerLinksTagHelper.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.TagHelpers;\n\n[HtmlTargetElement(\"pf-pagerLinks\", Attributes = \"controllerName, actionName, pagerContext\")]\npublic class PagerLinksTagHelper : TagHelper\n{\n\tprivate readonly IHtmlGenerator _htmlGenerator;\n\n\t[HtmlAttributeName(\"controllerName\")]\n\tpublic string ControllerName { get; set; }\n\t[HtmlAttributeName(\"actionName\")]\n\tpublic string ActionName { get; set; }\n\t[HtmlAttributeName(\"pagerContext\")]\n\tpublic PagerContext PagerContext { get; set; }\n\t[HtmlAttributeName(\"class\")]\n\tpublic string Class { get; set; }\n\t[HtmlAttributeName(\"moreTextClass\")]\n\tpublic string MoreTextClass { get; set; }\n\t[HtmlAttributeName(\"currentTextClass\")]\n\tpublic string CurrentTextClass { get; set; }\n\t[HtmlAttributeName(\"routeParameters\")]\n\tpublic Dictionary<string, object> RouteParameters { get; set; }\n\t[HtmlAttributeName(\"low\")]\n\tpublic int Low { get; set; }\n\t[HtmlAttributeName(\"high\")]\n\tpublic int High { get; set; }\n\n\t[HtmlAttributeNotBound]\n\t[ViewContext]\n\tpublic ViewContext ViewContext { get; set; }\n\n\tpublic PagerLinksTagHelper(IHtmlGenerator htmlGenerator)\n\t{\n\t\t_htmlGenerator = htmlGenerator;\n\t}\n\n\tpublic override void Process(TagHelperContext context, TagHelperOutput output)\n\t{\n\t\tif (PagerContext == null)\n\t\t{\n\t\t\toutput.TagName = \"div\";\n\t\t\toutput.TagMode = TagMode.StartTagAndEndTag;\n\t\t\treturn;\n\t\t}\n\t\tif (String.IsNullOrEmpty(ControllerName) || String.IsNullOrEmpty(ActionName))\n\t\t\tthrow new Exception(\"controllerName and actionName must be specified for PageLinks.\");\n\t\tif (PagerContext.PageCount <= 1)\n\t\t{\n\t\t\toutput.TagName = \"div\";\n\t\t\toutput.TagMode = TagMode.StartTagAndEndTag;\n\t\t\treturn;\n\t\t}\n\t\t\t\n\t\tvar builder = new StringBuilder();\n\t\tif (String.IsNullOrEmpty(MoreTextClass)) builder.Append($\"<li><span class=\\\"page-link\\\">{Resources.More}:</span></li>\");\n\t\telse builder.Append($\"<li><span class=\\\"page-link {MoreTextClass}\\\">{Resources.More}</span></li>\");\n\n\t\tif (PagerContext.PageIndex != 1 && Low != 1)\n\t\t{\n\t\t\t// first page link\n\t\t\tbuilder.Append(\"<li class=\\\"page-item\\\">\");\n\t\t\tvar firstRouteDictionary = new RouteValueDictionary(new { controller = ControllerName, action = ActionName, pageNumber = 1 });\n\t\t\tif (RouteParameters != null)\n\t\t\t\tforeach (var item in RouteParameters)\n\t\t\t\t\tfirstRouteDictionary.Add(item.Key, item.Value);\n\t\t\tvar firstLink = _htmlGenerator.GenerateActionLink(ViewContext, \"\", ActionName, ControllerName, null, null, null,\n\t\t\t\tfirstRouteDictionary, new { title = Resources.First, @class = \"page-link\" });\n\t\t\tfirstLink.InnerHtml.SetHtmlContent(\"<span class=\\\"icon icon-skip-start-fill\\\"></span>\");\n\t\t\tbuilder.Append(GetString(firstLink));\n\t\t\tbuilder.Append(\"</li>\");\n\t\t\tif (PagerContext.PageIndex > 2)\n\t\t\t{\n\t\t\t\t// previous page link\n\t\t\t\tvar previousIndex = PagerContext.PageIndex - 1;\n\t\t\t\tif (Low != 0)\n\t\t\t\t\tpreviousIndex = Low - 1;\n\t\t\t\tbuilder.Append(\"<li class=\\\"page-item\\\">\");\n\t\t\t\tvar previousRouteDictionary = new RouteValueDictionary(new { controller = ControllerName, action = ActionName, pageNumber = previousIndex });\n\t\t\t\tif (RouteParameters != null)\n\t\t\t\t\tforeach (var item in RouteParameters)\n\t\t\t\t\t\tpreviousRouteDictionary.Add(item.Key, item.Value);\n\t\t\t\tvar previousLink = _htmlGenerator.GenerateActionLink(ViewContext, \"\", ActionName, ControllerName, null, null, null, previousRouteDictionary, new { title = Resources.Previous, rel = \"prev\", @class = \"page-link\" });\n\t\t\t\tpreviousLink.InnerHtml.SetHtmlContent(\"<span class=\\\"icon icon-skip-backward-fill\\\"></span>\");\n\t\t\t\tbuilder.Append(GetString(previousLink));\n\t\t\t\tbuilder.Append(\"</li>\");\n\t\t\t}\n\t\t}\n\n\t\tif (Low == 0 || High == 0)\n\t\t{\n\t\t\t// not a multipage set of links used in partial page\n\t\t\t// calc low and high limits for numeric links\n\t\t\tvar low = PagerContext.PageIndex - 1;\n\t\t\tvar high = PagerContext.PageIndex + 3;\n\t\t\tif (low < 1) low = 1;\n\t\t\tif (high > PagerContext.PageCount) high = PagerContext.PageCount;\n\t\t\tif (high - low < 5) while ((high < low + 4) && high < PagerContext.PageCount) high++;\n\t\t\tif (high - low < 5) while ((low > high - 4) && low > 1) low--;\n\t\t\tfor (var x = low; x < high + 1; x++)\n\t\t\t{\n\t\t\t\t// numeric links\n\t\t\t\tif (x == PagerContext.PageIndex)\n\t\t\t\t{\n\t\t\t\t\tif (String.IsNullOrEmpty(CurrentTextClass))\n\t\t\t\t\t\tbuilder.Append($\"<li class=\\\"page-item active\\\"><span class=\\\"page-link\\\">{x} of {PagerContext.PageCount}</span></li>\");\n\t\t\t\t\telse builder.Append($\"<li class=\\\"page-item {CurrentTextClass}\\\"><span class=\\\"page-link\\\">{x} of {PagerContext.PageCount}</span></li>\");\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tbuilder.Append(\"<li>\");\n\t\t\t\t\tvar numericRouteDictionary = new RouteValueDictionary { { \"controller\", ControllerName }, { \"action\", ActionName }, { \"pageNumber\", x } };\n\t\t\t\t\tif (RouteParameters != null)\n\t\t\t\t\t\tforeach (var item in RouteParameters)\n\t\t\t\t\t\t\tnumericRouteDictionary.Add(item.Key, item.Value);\n\t\t\t\t\tvar link = _htmlGenerator.GenerateActionLink(ViewContext, x.ToString(), ActionName, ControllerName, null, null, null, numericRouteDictionary, new { @class = \"page-link\" });\n\t\t\t\t\tbuilder.Append(GetString(link));\n\t\t\t\t\tbuilder.Append(\"</li>\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// multipage set of links used in partial page\n\t\t\t// calc low and high limits for numeric links\n\t\t\tvar calcLow = PagerContext.PageIndex - 1;\n\t\t\tvar calcHigh = PagerContext.PageIndex + 3;\n\t\t\tif (calcLow < 1) calcLow = 1;\n\t\t\tif (calcHigh > PagerContext.PageCount) calcHigh = PagerContext.PageCount;\n\t\t\tif (calcHigh - calcLow < 5) while ((calcHigh < calcLow + 4) && calcHigh < PagerContext.PageCount) calcHigh++;\n\t\t\tif (calcHigh - calcLow < 5) while ((calcLow > calcHigh - 4) && calcLow > 1) calcLow--;\n\t\t\tvar isRangeRendered = false;\n\t\t\tfor (var x = calcLow; x < calcHigh + 1; x++)\n\t\t\t{\n\t\t\t\t// numeric links\n\t\t\t\tif (x >= Low && x <= High)\n\t\t\t\t{\n\t\t\t\t\tif (!isRangeRendered)\n\t\t\t\t\t{\n\t\t\t\t\t\tisRangeRendered = true;\n\t\t\t\t\t\tif (String.IsNullOrEmpty(CurrentTextClass))\n\t\t\t\t\t\t\tbuilder.Append($\"<li class=\\\"active\\\"><span class=\\\"page-link\\\">{Low}-{High} of {PagerContext.PageCount}</span></li>\");\n\t\t\t\t\t\telse builder.Append($\"<li class=\\\"active {CurrentTextClass}\\\"><span class=\\\"page-link\\\">{Low}-{High} of {PagerContext.PageCount}</span></li>\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tbuilder.Append(\"<li class=\\\"page-item\\\">\");\n\t\t\t\t\tvar numericRouteDictionary = new RouteValueDictionary { { \"controller\", ControllerName }, { \"action\", ActionName }, { \"pageNumber\", x } };\n\t\t\t\t\tif (RouteParameters != null)\n\t\t\t\t\t\tforeach (var item in RouteParameters)\n\t\t\t\t\t\t\tnumericRouteDictionary.Add(item.Key, item.Value);\n\t\t\t\t\tvar link = _htmlGenerator.GenerateActionLink(ViewContext, x.ToString(), ActionName, ControllerName, null, null, null, numericRouteDictionary, new { @class = \"page-link\" });\n\t\t\t\t\tbuilder.Append(GetString(link));\n\t\t\t\t\tbuilder.Append(\"</li>\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (PagerContext.PageIndex != PagerContext.PageCount && High < PagerContext.PageCount)\n\t\t{\n\t\t\tif (PagerContext.PageIndex < PagerContext.PageCount - 1)\n\t\t\t{\n\t\t\t\t// next page link\n\t\t\t\tvar nextIndex = PagerContext.PageIndex + 1;\n\t\t\t\tbuilder.Append(\"<li class=\\\"page-item\\\">\");\n\t\t\t\tvar nextRouteDictionary = new RouteValueDictionary(new { controller = ControllerName, action = ActionName, pageNumber = nextIndex });\n\t\t\t\tif (RouteParameters != null)\n\t\t\t\t\tforeach (var item in RouteParameters)\n\t\t\t\t\t\tnextRouteDictionary.Add(item.Key, item.Value);\n\t\t\t\tvar nextLink = _htmlGenerator.GenerateActionLink(ViewContext, \"\", ActionName, ControllerName, null, null, null, nextRouteDictionary, new { title = Resources.Next, rel = \"next\", @class = \"page-link\" });\n\t\t\t\tnextLink.InnerHtml.SetHtmlContent(\"<span class=\\\"icon icon-skip-forward-fill\\\"></span>\");\n\t\t\t\tbuilder.Append(GetString(nextLink));\n\t\t\t\tbuilder.Append(\"</li>\");\n\t\t\t}\n\t\t\t// last page link\n\t\t\tbuilder.Append(\"<li class=\\\"page-item\\\">\");\n\t\t\tvar lastRouteDictionary = new RouteValueDictionary(new { controller = ControllerName, action = ActionName, pageNumber = PagerContext.PageCount });\n\t\t\tif (RouteParameters != null)\n\t\t\t\tforeach (var item in RouteParameters)\n\t\t\t\t\tlastRouteDictionary.Add(item.Key, item.Value);\n\t\t\tvar lastLink = _htmlGenerator.GenerateActionLink(ViewContext, \"\", ActionName, ControllerName, null, null, null, lastRouteDictionary, new { title = Resources.Last, @class = \"page-link\" });\n\t\t\tlastLink.InnerHtml.SetHtmlContent(\"<span class=\\\"icon icon-skip-end-fill\\\"></span>\");\n\t\t\tbuilder.Append(GetString(lastLink));\n\t\t\tbuilder.Append(\"</li>\");\n\t\t}\n\n\t\toutput.TagMode = TagMode.StartTagAndEndTag;\n\t\toutput.TagName = \"ul\";\n\t\tif (!String.IsNullOrWhiteSpace(Class))\n\t\t\toutput.Attributes.Add(\"class\", Class);\n\t\toutput.Content.AppendHtml(builder.ToString());\n\t}\n\n\tprivate static string GetString(IHtmlContent content)\n\t{\n\t\tvar writer = new System.IO.StringWriter();\n\t\tcontent.WriteTo(writer, HtmlEncoder.Default);\n\t\treturn writer.ToString();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/TagHelpers/TopicReadIndicatorTagHelper.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.TagHelpers;\n\n[HtmlTargetElement(\"pf-topicReadIndicator\", Attributes = \"topic, pagedTopicContainer\")]\npublic class TopicReadIndicatorTagHelper : TagHelper\n{\n\t[HtmlAttributeName(\"topic\")]\n\tpublic Topic Topic { get; set; }\n\t[HtmlAttributeName(\"pagedTopicContainer\")]\n\tpublic PagedTopicContainer PagedTopicContainer { get; set; }\n\t[HtmlAttributeName(\"class\")]\n\tpublic string Class { get; set; }\n\n\tpublic override void Process(TagHelperContext context, TagHelperOutput output)\n\t{\n\t\tvar alt = Resources.NoNewPosts;\n\t\tif (PagedTopicContainer.ReadStatusLookup.ContainsKey(Topic.TopicID))\n\t\t{\n\t\t\tvar status = PagedTopicContainer.ReadStatusLookup[Topic.TopicID];\n\t\t\tswitch (status)\n\t\t\t{\n\t\t\t\tcase ReadStatus.Open | ReadStatus.NewPosts | ReadStatus.Pinned:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text-fill text-warning\\\"></span><span class=\\\"icon icon-pin-angle-fill soloLeftBadge topicIndicatorBadge text-success\\\"></span>\");\n\t\t\t\t\talt = Resources.NewPostsPinned;\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.Open | ReadStatus.NewPosts | ReadStatus.NotPinned:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text-fill text-warning\\\"></span>\");\n\t\t\t\t\talt = Resources.NewPosts;\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.Open | ReadStatus.NoNewPosts | ReadStatus.Pinned:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text text-muted\\\"></span><span class=\\\"icon icon-pin-angle-fill soloLeftBadge topicIndicatorBadge text-success\\\"></span>\");\n\t\t\t\t\talt = Resources.Pinned;\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.Open | ReadStatus.NoNewPosts | ReadStatus.NotPinned:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text text-muted\\\"></span>\");\n\t\t\t\t\talt = Resources.NoNewPosts;\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.Closed | ReadStatus.NewPosts | ReadStatus.Pinned:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text-fill text-warning\\\"></span><span class=\\\"icon icon-lock-fill firstBadge topicIndicatorBadge text-danger\\\"></span><span class=\\\"icon icon-pin-angle-fill secondBadge topicIndicatorBadge text-success\\\"></span>\");\n\t\t\t\t\talt = Resources.NewPostsClosedPinned;\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.Closed | ReadStatus.NewPosts | ReadStatus.NotPinned:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text-fill text-warning\\\"></span><span class=\\\"icon icon-lock-fill firstBadge topicIndicatorBadge text-danger\\\"></span>\");\n\t\t\t\t\talt = Resources.NewPostsClosed;\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.Closed | ReadStatus.NoNewPosts | ReadStatus.Pinned:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text text-muted\\\"></span><span class=\\\"icon icon-lock-fill firstBadge topicIndicatorBadge text-danger\\\"></span><span class=\\\"icon icon-pin-angle-fill secondBadge topicIndicatorBadge text-success\\\"></span>\");\n\t\t\t\t\talt = Resources.ClosedPinned;\n\t\t\t\t\tbreak;\n\t\t\t\tcase ReadStatus.Closed | ReadStatus.NoNewPosts | ReadStatus.NotPinned:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text text-muted\\\"></span><span class=\\\"icon icon-lock-fill firstBadge topicIndicatorBadge text-danger\\\"></span>\");\n\t\t\t\t\talt = Resources.Closed;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\toutput.PostElement.AppendHtml(\"<span class=\\\"icon icon-file-earmark-text text-muted\\\"></span>\");\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\toutput.TagName = \"div\";\n\t\toutput.Attributes.Add(\"title\", alt);\n\t\tif (!String.IsNullOrWhiteSpace(Class))\n\t\t\toutput.Attributes.Add(\"class\", $\"topicIndicator {Class}\");\n\t\telse\n\t\t\toutput.Attributes.Add(\"class\", \"topicIndicator\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/TagHelpers/ValidationClassTagHelper.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.TagHelpers;\n\n[HtmlTargetElement(\"div\", Attributes = ValidationForAttributeName + \",\" + ValidationErrorClassName)]\npublic class ValidationClassTagHelper : TagHelper\n{\n\tprivate const string ValidationForAttributeName = \"pf-validation-for\";\n\tprivate const string ValidationErrorClassName = \"pf-validationerror-class\";\n\n\t[HtmlAttributeName(ValidationForAttributeName)]\n\tpublic ModelExpression For { get; set; }\n\n\t[HtmlAttributeName(ValidationErrorClassName)]\n\tpublic string ValidationErrorClass { get; set; }\n\n\t[HtmlAttributeNotBound]\n\t[ViewContext]\n\tpublic ViewContext ViewContext { get; set; }\n\n\tpublic override void Process(TagHelperContext context, TagHelperOutput output)\n\t{\n\t\tModelStateEntry entry;\n\t\tViewContext.ViewData.ModelState.TryGetValue(For.Name, out entry);\n\t\tif (entry == null || !entry.Errors.Any())\n\t\t\treturn;\n\t\tvar tagBuilder = new TagBuilder(\"div\");\n\t\ttagBuilder.AddCssClass(ValidationErrorClass);\n\t\toutput.MergeAttributes(tagBuilder);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/ViewComponents/UserNavigationViewComponent.cs",
    "content": "﻿#pragma warning disable CS1998\nnamespace PopForums.Mvc.Areas.Forums.ViewComponents;\n\npublic class UserNavigationViewComponent : ViewComponent\n{\n\tpublic async Task<IViewComponentResult> InvokeAsync()\n\t{\n\t\treturn View();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/ViewComponents/UserStateViewComponent.cs",
    "content": "﻿namespace PopForums.Mvc.Areas.Forums.ViewComponents;\n\npublic class UserStateViewComponent : ViewComponent\n{\n\tprivate readonly IUserStateComposer _userStateComposer;\n\n\tpublic UserStateViewComponent(IUserStateComposer userStateComposer)\n\t{\n\t\t_userStateComposer = userStateComposer;\n\t}\n\n\tpublic async Task<IViewComponentResult> InvokeAsync()\n\t{\n\t\tvar container = await _userStateComposer.GetState();\n\t\treturn View(container);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/AccountCreated.cshtml",
    "content": "﻿@{\n\tViewBag.Title = PopForums.Resources.AccountCreated;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.AccountCreated</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a> </li>\n</ul>\n\n@if (ViewData[\"EmailProblem\"] == null)\n{\n\t<p class=\"alert alert-success\">@ViewData[\"Result\"]</p>\n}\nelse\n{\n\t<p class=\"alert alert-danger\">@ViewData[\"EmailProblem\"] @ViewData[\"Result\"]</p>\n}\n\n<p><a asp-action=\"EditProfile\">@PopForums.Resources.EditYourProfile</a></p>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/Create.cshtml",
    "content": "﻿@using PopForums.Configuration\n@model SignupData\n@inject IUserRetrievalShim UserRetrievalShim\n@inject IConfig Config\n@inject ISettingsManager SettingsManager\n@{\n\tViewBag.Title = PopForums.Resources.CreateAnAccount;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n@section HeaderContent\n{\n\t@if (Config.UseReCaptcha)\n\t{\n\t\t<script src=\"https://www.google.com/recaptcha/api.js?render=@Config.ReCaptchaSiteKey\"></script>\n\t\t<script language=\"javascript\" type=\"text/javascript\">\n\t\t\tgrecaptcha.ready(function() {\n\t\t\t\tgrecaptcha.execute(\"@Config.ReCaptchaSiteKey\", { action: \"createaccount\" }).then(function(token) {\n\t\t\t\t\tdocument.querySelector(\"#Token\").value = token;\n\t\t\t\t});\n\t\t\t});\n\t\t</script>\n\t}\n}\n\n<h1>@PopForums.Resources.CreateAnAccount</h1>\n@if (!SettingsManager.Current.IsPrivateForumInstance)\n{\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t</ul>\n}\n\n<p><a asp-action=\"Verify\">@PopForums.Resources.NeedToVerifyExistingAccount</a></p>\n\n@if (user == null)\n{\n\t<form asp-controller=\"@AccountController.Name\" asp-action=\"Create\" method=\"post\" role=\"form\">\n\t\t<div class=\"mb-3 row\" pf-validation-for=\"Name\" pf-validationerror-class=\"has-error\">\n\t\t\t<label for=\"Name\" class=\"col-sm-2 form-label\">@PopForums.Resources.Name @PopForums.Resources.AsYouWouldLikeItToAppear</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<input asp-for=\"Name\" class=\"form-control\"/>\n\t\t\t\t<span asp-validation-for=\"Name\" class=\"text-danger form-text fw-bold\"></span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\" pf-validation-for=\"Email\" pf-validationerror-class=\"has-error\">\n\t\t\t<label for=\"Email\" class=\"col-sm-2 form-label\">@PopForums.Resources.Email</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<input asp-for=\"Email\" class=\"form-control\" type=\"email\" />\n\t\t\t\t<span asp-validation-for=\"Email\" class=\"text-danger form-text fw-bold\"></span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\" pf-validation-for=\"Password\" pf-validationerror-class=\"has-error\">\n\t\t\t<label for=\"Password\" class=\"col-sm-2 form-label\">@PopForums.Resources.Password @PopForums.Resources.AtLeastSixChar</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<input type=\"password\" asp-for=\"Password\" class=\"form-control\" />\n\t\t\t\t<span asp-validation-for=\"Password\" class=\"text-danger form-text fw-bold\"></span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\" pf-validation-for=\"PasswordRetype\" pf-validationerror-class=\"has-error\">\n\t\t\t<label for=\"PasswordRetype\" class=\"col-sm-2 form-label\">@PopForums.Resources.RetypePassword</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<input type=\"password\" asp-for=\"PasswordRetype\" class=\"form-control\" />\n\t\t\t\t<span asp-validation-for=\"PasswordRetype\" class=\"text-danger form-text fw-bold\"></span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" asp-for=\"IsSubscribed\" />\n\t\t\t\t<label class=\"form-check-label\">@PopForums.Resources.SubscribeToList</label>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" asp-for=\"IsAutoFollowOnReply\" />\n\t\t\t\t<label class=\"form-check-label\">@PopForums.Resources.AutoFollow</label>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\" pf-validation-for=\"IsCoppa\" pf-validationerror-class=\"has-error\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" asp-for=\"IsCoppa\" />\n\t\t\t\t<label class=\"form-check-label\">@String.Format(PopForums.Resources.MyBirthdayIsOnOrBefore, ViewData[AccountController.CoppaDateKey])</label>\n\t\t\t\t<span asp-validation-for=\"IsCoppa\" class=\"text-danger\"></span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label class=\"col-sm-2 form-label\">\n\t\t\t\t@PopForums.Resources.TermsOfService\n\t\t\t</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<textarea class=\"form-control\" disabled=\"disabled\" rows=\"6\" id=\"TOS\">@ViewData[AccountController.TosKey]</textarea>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\" pf-validation-for=\"IsTos\" pf-validationerror-class=\"has-error\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" asp-for=\"IsTos\" />\n\t\t\t\t<label class=\"form-check-label\">@PopForums.Resources.HaveReadTOS</label>\n\t\t\t\t<span asp-validation-for=\"IsTos\" class=\"text-danger\"></span>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input type=\"submit\" value=\"@PopForums.Resources.CreateAccountButton\" class=\"btn btn-primary my-3\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<input type=\"hidden\" asp-for=\"Token\" id=\"Token\" />\n\t</form>\n}\nelse\n{\n\t<p class=\"alert alert-danger\">@PopForums.Resources.AlreadyCreatedAccount</p>\n}\n\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/EditAccountNoUser.cshtml",
    "content": "﻿@{\n\tViewBag.Title = PopForums.Resources.EditAccount;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.EditAccount</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n</ul>\n\n<p>@PopForums.Resources.MustBeRegisteredToEditAccount</p>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/EditProfile.cshtml",
    "content": "﻿@model UserEditProfile\n@inject IUserRetrievalShim UserRetrievalShim\n@inject IConfig Config\n@{\n\tViewBag.Title = PopForums.Resources.Account + \" - \" + PopForums.Resources.EditProfile;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n@section HeaderContent\n{\n    <script type=\"text/javascript\" src=\"~/PopForums/lib/tinymce/tinymce.min.js\" asp-append-version=\"true\"></script>\n}\n\n<h1>@PopForums.Resources.Account - @PopForums.Resources.EditProfile</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item\">@PopForums.Resources.Account</li>\n\t<li class=\"breadcrumb-item dropdown\">\n\t\t<a class=\"dropdown-toggle\" data-bs-toggle=\"dropdown\" href=\"#\" aria-haspopup=\"true\" aria-expanded=\"false\" id=\"SubProfileMenu\">@PopForums.Resources.EditProfile</a>\n\t\t<div class=\"dropdown-menu\" aria-labelledby=\"SubProfileMenu\">\n\t\t\t@if (!Config.IsOAuthOnly)\n\t\t\t{\n\t\t\t\t<a class=\"dropdown-item\" asp-action=\"Security\">@PopForums.Resources.ChangeYourEmailPassword</a>\n\t\t\t}\n\t\t\t<a class=\"dropdown-item\" asp-action=\"ManagePhotos\">@PopForums.Resources.ManageYourPhotos</a>\n\t\t\t@if (!Config.IsOAuthOnly)\n\t\t\t{\n\t\t\t\t<a class=\"dropdown-item\" asp-action=\"ExternalLogins\">@PopForums.Resources.ExternalLogins</a>\n\t\t\t}\n\t\t</div>\n\t</li>\n</ul>\n\n@if (ViewData[\"Result\"] != null)\n{ <p class=\"alert alert-success\">@ViewData[\"Result\"]</p> }\n\n@if (!user.IsApproved)\n{ <p class=\"alert alert-warning\"><a asp-action=\"Verify\">@PopForums.Resources.VerifyAccount</a></p> }\n\n<form asp-action=\"EditProfile\" role=\"form\" method=\"post\">\n\t  \n<h2>@PopForums.Resources.Options</h2>\n\n  <div class=\"mb-3 row\">\n  \t<div class=\"offset-sm-2 col-sm-10\">\n  \t\t<input class=\"form-check-input\" type=\"checkbox\" asp-for=\"IsSubscribed\" />\n  \t\t<label class=\"form-check-label\" for=\"IsSubscribed\">@PopForums.Resources.SubscribeToList</label>\n\t</div>\n  </div>\n  <div class=\"mb-3 row\">\n\t  <div class=\"offset-sm-2 col-sm-10\">\n\t\t  <input class=\"form-check-input\" type=\"checkbox\" asp-for=\"IsAutoFollowOnReply\" />\n\t\t  <label class=\"form-check-label\" for=\"IsAutoFollowOnReply\">@PopForums.Resources.AutoFollow</label>\n\t  </div>\n  </div>\n  <div class=\"mb-3 row\">\n  \t<div class=\"offset-sm-2 col-sm-10\">\n  \t\t<input class=\"form-check-input\" type=\"checkbox\" asp-for=\"ShowDetails\" />\n  \t\t<label class=\"form-check-label\" for=\"ShowDetails\">@PopForums.Resources.ShowProfileDetails</label>\n  </div>\n  </div>\n  <div class=\"mb-3 row\">\n  \t<div class=\"offset-sm-2 col-sm-10\">\n  \t\t<input class=\"form-check-input\" type=\"checkbox\" asp-for=\"IsPlainText\" />\n  \t\t<label class=\"form-check-label\" for=\"IsPlainText\">@PopForums.Resources.ForcePlainTextBox</label>\n  </div>\n  </div>\n  <div class=\"mb-3 row\">\n  \t<div class=\"offset-sm-2 col-sm-10\">\n  \t\t<input class=\"form-check-input\" type=\"checkbox\" asp-for=\"HideVanity\" />\n  \t\t<label class=\"form-check-label\" for=\"HideVanity\">@PopForums.Resources.HideVanity</label>\n  </div>\n  </div>\n\n  <div class=\"row\">\n  \t<h2>@PopForums.Resources.Details</h2>\n  </div>\n  <div class=\"mb-3 row\">\n  \t<label for=\"Signature\" class=\"col-sm-2 form-label\">@PopForums.Resources.Signature</label>\n\t  <div class=\"col-sm-10\">\n\t\t  <pf-fulltext formid=\"Signature\" overridelistener=\"true\" forcenoimage=\"true\" isshort=\"true\" value=\"@Model.Signature\"></pf-fulltext>\n\t  </div>\n  </div>\n  <div class=\"mb-3 row\">\n  \t<label for=\"Location\" class=\"col-sm-2 form-label\">@PopForums.Resources.Location</label>\n  <div class=\"col-sm-10\">\n  \t<input type=\"text\" asp-for=\"Location\" class=\"form-control\" />\n  </div>\n  </div>\n  <div class=\"mb-3 row\">\n  \t<label for=\"Dob\" class=\"col-sm-2 form-label\">@PopForums.Resources.DateOfBirth</label>\n  <div class=\"col-sm-10\">\n  \t<input type=\"text\" asp-for=\"Dob\" asp-format=\"{0:d}\" class=\"form-control\" />\n  </div>\n  </div>\n  <div class=\"mb-3 row\">\n\t  <label for=\"Facebook\" class=\"col-sm-2 form-label\"><span class=\"icon icon-facebook\"></span> Facebook</label>\n  \t<div class=\"col-sm-10\">\n  \t\t<div class=\"input-group\">\n  \t\t\t<div class=\"input-group-text\">https://facebook.com/</div>\n  \t\t\t<input type=\"text\" asp-for=\"Facebook\" class=\"form-control\" />\n  \t\t</div>\n  \t</div>\n  </div>\n\t  <div class=\"mb-3 row\">\n\t\t  <label for=\"Instagram\" class=\"col-sm-2 form-label\"><span class=\"icon icon-instagram\"></span> Instagram</label>\n\t\t  <div class=\"col-sm-10\">\n\t\t\t  <div class=\"input-group\">\n\t\t\t\t  <div class=\"input-group-text\">&#64;</div>\n\t\t\t\t  <input type=\"text\" asp-for=\"Instagram\" class=\"form-control\" /></div>\n\t\t  </div>\n\t  </div>\n\t  <div class=\"mb-3 row\">\n  \t<label for=\"Web\" class=\"col-sm-2 form-label\">@PopForums.Resources.Web</label>\n  <div class=\"col-sm-10\">\n  \t<input type=\"text\" asp-for=\"Web\" class=\"form-control\" />\n  </div>\n  </div>\n  <div class=\"mb-3 offset-sm-2 col-sm-10\">\n  \t<input type=\"submit\" value=\"@PopForums.Resources.Save\" class=\"btn btn-primary\" />\n  </div>\n  </form>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/ExternalLogins.cshtml",
    "content": "﻿@model List<PopForums.ExternalLogin.ExternalUserAssociation>\n@{\n\tViewBag.Title = PopForums.Resources.Account + \" - \" + PopForums.Resources.ExternalLogins;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n\n<h1>@PopForums.Resources.Account - @PopForums.Resources.ExternalLogins</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item\">@PopForums.Resources.Account</li>\n\t<li class=\"breadcrumb-item dropdown\">\n\t\t<a class=\"dropdown-toggle\" data-bs-toggle=\"dropdown\" href=\"#\" aria-haspopup=\"true\" aria-expanded=\"false\" id=\"SubProfileMenu\">@PopForums.Resources.ExternalLogins</a>\n\t\t<div class=\"dropdown-menu\" aria-labelledby=\"SubProfileMenu\">\n\t\t\t<a class=\"dropdown-item\" asp-action=\"EditProfile\">@PopForums.Resources.EditYourProfile</a>\n\t\t\t<a class=\"dropdown-item\" asp-action=\"Security\">@PopForums.Resources.ChangeYourEmailPassword</a>\n\t\t\t<a class=\"dropdown-item\" asp-action=\"ManagePhotos\">@PopForums.Resources.ManageYourPhotos</a>\n\t\t</div>\n\t</li>\n\n</ul>\n\n@if (Model.Count == 0)\n{\n\t<p class=\"alert alert-warning\">@PopForums.Resources.NoExternalLoginsRegistered</p>\n}\nelse\n{\n\t<table class=\"table mt-3 table-hover\">\n\t\t@foreach (var item in Model)\n\t\t{\n\t\t\t<tr class=\"row\">\n\t\t\t\t<td class=\"col-3\">\n\t\t\t\t\t<form asp-action=\"RemoveExternalLogin\" asp-route-id=\"@item.ExternalUserAssociationID\" method=\"post\">\n\t\t\t\t\t\t<input type=\"submit\" value=\"Remove\" class=\"btn btn-primary\" />\n\t\t\t\t\t</form>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"col-3\">@item.Issuer</td>\n\t\t\t\t<td class=\"col-6\">@item.Name</td>\n\t\t\t</tr>\n\t\t}\n\t</table>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/Forgot.cshtml",
    "content": "﻿@using PopForums.Configuration\n@inject ISettingsManager SettingsManager\n@{\n    ViewBag.Title = PopForums.Resources.ForgotPassword;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.ForgotPassword</h1>\n@if (!SettingsManager.Current.IsPrivateForumInstance)\n{\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t</ul>\n}\n\n<p>@PopForums.Resources.ForgotInstructions</p>\n\n<form asp-action=\"Forgot\" method=\"post\" role=\"form\">\n\t<div class=\"mb-3\">\n\t\t<input name=\"Email\" class=\"form-control\"/>\n\t</div>\n\t<input type=\"submit\" value=\"@PopForums.Resources.SendEmailButton\" class=\"btn btn-primary mb-3\" />\n\t@if (ViewBag.Result != null) { <span class=\"alert alert-warning ml-3\">@ViewBag.Result</span> }\n</form>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/Login.cshtml",
    "content": "﻿@inject IUserRetrievalShim UserRetrievalShim\n@inject ISettingsManager SettingsManager\n@using PopForums.Configuration\n@model Dictionary<PopIdentity.ProviderType, ExternalLoginTypeMetadata>\n@{\n    ViewBag.Title = PopForums.Resources.Login;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n<h1>@PopForums.Resources.Login</h1>\n@if (!SettingsManager.Current.IsPrivateForumInstance)\n{\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t</ul>\n}\n\n<div id=\"LoginResult\" class=\"my-3 alert alert-danger @if (String.IsNullOrWhiteSpace(Context.Request.Query[\"error\"])){<text> d-none</text>}\">@Context.Request.Query[\"error\"]</div>\n\n@if (user == null)\n{\n\t<template id=\"LoginForm\">\n\t\t<div role=\"form\">\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label for=\"EmailLogin\" class=\"form-label\">@PopForums.Resources.Email</label>\n\t\t\t\t<input type=\"text\" name=\"EmailLogin\" id=\"EmailLogin\" class=\"form-control\"/>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label for=\"PasswordLogin\" class=\"form-label\">@PopForums.Resources.Password</label>\n\t\t\t\t<input type=\"password\" name=\"PasswordLogin\" id=\"PasswordLogin\" class=\"form-control\"/>\n\t\t\t</div>\n\t\t\t<input id=\"LoginButton\" type=\"button\" value=\"@PopForums.Resources.Login\" class=\"btn btn-primary mb-3\" />\n\t\t</div>\n\t</template>\n\n\t<pf-loginform templateid=\"LoginForm\" isexternallogin=\"false\"></pf-loginform>\n\n\tif (Model.Count > 0)\n\t{\n\t\t<form asp-controller=\"Identity\" asp-action=\"ExternalLogin\" asp-route-ReturnUrl=\"@ViewBag.Referrer\">\n\t\t\t@Html.AntiForgeryToken()\n\t\t\t<h2>@PopForums.Resources.ExternalLogins</h2>\n\t\t\t<p>\n\t\t\t\t@foreach (var item in Model)\n\t\t\t\t{\n\t\t\t\t\t<button type=\"submit\" id=\"@item.Key\" name=\"provider\" value=\"@item.Key\" class=\"btn btn-primary\"><span class=\"icon @item.Value.CssClass\"></span> @item.Value.Name</button>\n\t\t\t\t}\n\t\t\t</p>\n\t\t</form>\n\t}\n\n\t<p>@PopForums.Resources.NotRegisteredQuestion <a asp-action=\"Create\">@PopForums.Resources.CreateAnAccount</a>. <a asp-action=\"Forgot\">@PopForums.Resources.ForgotPasswordQuestion</a></p>\n\n\t<input type=\"hidden\" id=\"Referrer\" value=\"@((object)ViewBag.Referrer)\"/>\n}\nelse\n{\n\t<p>@PopForums.Resources.LoginAlready</p>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/ManagePhotos.cshtml",
    "content": "﻿@model UserEditPhoto\n@inject IConfig Config\n\n@{\n\tViewBag.Title = PopForums.Resources.Account + \" - \" + PopForums.Resources.ManagePhotos;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.Account - @PopForums.Resources.ManagePhotos</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item\">@PopForums.Resources.Account</li>\n\t<li class=\"breadcrumb-item dropdown\">\n\t\t<a class=\"dropdown-toggle\" data-bs-toggle=\"dropdown\" href=\"#\" aria-haspopup=\"true\" aria-expanded=\"false\" id=\"SubProfileMenu\">@PopForums.Resources.ManagePhotos</a>\n\t\t<div class=\"dropdown-menu\" aria-labelledby=\"SubProfileMenu\">\n\t\t\t<a class=\"dropdown-item\" asp-action=\"EditProfile\">@PopForums.Resources.EditYourProfile</a>\n\t\t\t@if (!Config.IsOAuthOnly)\n\t\t\t{\n\t\t\t\t<a class=\"dropdown-item\" asp-action=\"Security\">@PopForums.Resources.ChangeYourEmailPassword</a>\n\t\t\t\t<a class=\"dropdown-item\" asp-action=\"ExternalLogins\">@PopForums.Resources.ExternalLogins</a>\n\t\t\t}\n\t\t</div>\n\t</li>\n</ul>\n\n<form asp-action=\"ManagePhotos\" method=\"post\" enctype=\"multipart/form-data\" role=\"form\">\n\t<h2>@PopForums.Resources.Avatar</h2>\n\t<div class=\"mb-3\">\n\t\t<input type=\"file\" class=\"form-control\" id=\"avatarFile\" name=\"avatarFile\">\n\t</div>\n\t@if (Model.AvatarID.HasValue)\n\t{\n\t\t<p><img src=\"@Url.Action(\"Avatar\", \"Image\", new {id = Model.AvatarID})\" alt=\"Avatar image\" /></p>\n\t\t<div class=\"mb-3\">\n\t\t\t<input type=\"checkbox\" asp-for=\"DeleteAvatar\" class=\"form-check-input\" />\n\t\t\t<label for=\"DeleteAvatar\" class=\"form-check-label\">@PopForums.Resources.AvatarDelete</label>\n\t\t</div>\n\t}\n\n\t<h2>@PopForums.Resources.Photo</h2>\n\t<div class=\"mb-3\">\n\t\t<input type=\"file\" class=\"form-control\" id=\"photoFile\" name=\"photoFile\">\n\t</div>\n\t@if (Model.ImageID.HasValue)\n\t{\n\t\t<p><img src=\"@Url.Action(\"UserImage\", \"Image\", new {id = Model.ImageID})\" alt=\"User image\" /></p>\n\t\t<div class=\"mb-3\">\n\t\t\t<input type=\"checkbox\" asp-for=\"DeleteImage\" class=\"form-check-input\" />\n\t\t\t<label for=\"DeleteImage\" class=\"form-check-label\"> @PopForums.Resources.PhotoDelete</label>\n\t\t</div>\n\t\tif (Model.IsImageApproved.HasValue && !Model.IsImageApproved.Value)\n\t\t{\n\t\t\t<p class=\"alert alert-warning\">@PopForums.Resources.PhotoNotApproved</p>\n\t\t}\n\t}\n\n\t<input type=\"submit\" value=\"@PopForums.Resources.Save\" class=\"btn btn-primary my-3\" />\n</form>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/MiniProfile.cshtml",
    "content": "﻿@model DisplayProfile\n\n<div class=\"miniProfile\">\n\t@PopForums.Resources.Joined: <pf-formattedtime utctime=\"@Model.Joined.ToString(\"o\")\"></pf-formattedtime><br />\n\t@if (!String.IsNullOrWhiteSpace(Model.Location))\n\t{\n\t\t@PopForums.Resources.Location<text>: </text>@Model.Location<br />\n\t}\n\t@PopForums.Resources.Posts: <a asp-controller=\"Account\" asp-action=\"Posts\" asp-route-id=\"@Model.UserID\" asp-route-pageNumber=\"\" target=\"_blank\">@Model.PostCount.ToString(\"N0\")</a>, @PopForums.Resources.ScoringGame: @Model.Points.ToString(\"N0\")<br />\n\t@if (!String.IsNullOrWhiteSpace(Model.Facebook))\n\t{\n\t\t<a href=\"https://facebook.com/@Model.Facebook\" target=\"_blank\" class=\"icon\"><span class=\"icon icon-facebook\"></span></a>\n\t}\n\t@if (!String.IsNullOrWhiteSpace(Model.Instagram))\n\t{\n\t\t<a href=\"https://www.instagram.com/@Model.Instagram\" target=\"_blank\" class=\"icon\"><span class=\"icon icon-instagram\"></span></a>\n\t}\n\t<a asp-action=\"ViewProfile\" asp-route-id=\"@Model.UserID\" target=\"_blank\">Full Profile</a>\n\t@if (Model.ShowDetails)\n\t{@:|\n\t\t<a asp-controller=\"PrivateMessages\" asp-action=\"Create\" asp-route-id=\"@Model.UserID\">@PopForums.Resources.SendPM</a> \n\t}\n\t@if (!String.IsNullOrWhiteSpace(Model.Web))\n\t{\n\t\t<text>| </text><a href=\"@Model.Web\" target=\"_blank\">@PopForums.Resources.WebVisit</a>\n\t}\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/MiniUserNotFound.cshtml",
    "content": "﻿<p>@PopForums.Resources.UserNotFound</p>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/OAuthLogin.cshtml",
    "content": "@using PopForums\n@model string\n@inject ISettingsManager SettingsManager\n\n@{\n\tLayout = null;\n}\n\n<!DOCTYPE html>\n\n<html>\n<head>\n\t<title>@Resources.Login</title>\n\t<link href=\"~/PopForums/lib/bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" asp-append-version=\"true\" />\n</head>\n<body>\n<div class=\"vh-100 vw-100 p-5 d-flex flex-column justify-content-center\">\n\t<h1 class=\"mx-auto mb-3\">@SettingsManager.Current.ForumTitle</h1>\n\t<div class=\"alert alert-primary d-flex justify-content-center\">\n\t\t<a href=\"@Model\" class=\"btn btn-primary\">@Resources.Login</a>\n\t</div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/Posts.cshtml",
    "content": "﻿@model PagedTopicContainer\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tViewBag.Title = ViewBag.PostUserName + \"'s \" + PopForums.Resources.Posts;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n<h1>@ViewBag.PostUserName's @PopForums.Resources.Posts</h1>\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item\">@ViewBag.PostUserName's @PopForums.Resources.Posts</li>\n</ul>\n\n<pf-pagerLinks controllerName=\"Account\" actionName=\"Posts\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n<table id=\"TopicList\" class=\"table mt-3 table-hover\">\n\t@foreach (var topic in Model.Topics)\n\t{\n\t\t<tr @if (topic.IsDeleted) { <text> class=\"bg-danger\" </text> \t\t\t\t} data-topicid=\"@topic.TopicID\">\n\t\t\t<td class=\"newIndicator\"><a asp-controller=\"Forum\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\" /></a></td>\n\t\t\t<td>\n\t\t\t\t<h2>@Html.ActionLink(topic.Title, \"Topic\", \"Forum\", new { id = topic.UrlName, pageNumber = 1 }, null)</h2>\n\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t\t\t@PopForums.Resources.StartedBy: @topic.StartedByName @PopForums.Resources.In @Model.ForumTitles[topic.ForumID] | @PopForums.Resources.Views: @topic.ViewCount | @PopForums.Resources.Replies: @topic.ReplyCount | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@topic.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By @topic.LastPostName\n\t\t\t\t</small>\n\t\t\t</td>\n\t\t</tr>\n\t}\n</table>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/ResetPassword.cshtml",
    "content": "﻿@model PasswordResetContainer\n@{\n    ViewBag.Title = PopForums.Resources.PasswordReset;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.PasswordReset</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n</ul>\n\n@if (Model.IsValidUser)\n{\n\tusing(Html.BeginForm()){\n\t\t<div role=\"form\">\n\t\t\t<div class=\"mb-3\" pf-validationerror-class=\"has-error\" pf-validation-for=\"Password\">\n\t\t\t\t<label for=\"Password\" class=\"form-label\">@PopForums.Resources.PasswordNew</label>\n\t\t\t\t<input type=\"password\" asp-for=\"Password\" class=\"form-control\"/>\n\t\t\t\t<span asp-validation-for=\"Password\"></span>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\" pf-validationerror-class=\"has-error\" pf-validation-for=\"PasswordRetype\">\n\t\t\t\t<label for=\"PasswordRetype\" class=\"form-label\">@PopForums.Resources.PasswordNewRetype</label>\n\t\t\t\t<input type=\"password\" asp-for=\"PasswordRetype\" class=\"form-control\"/>\n\t\t\t\t<span asp-validation-for=\"PasswordRetype\"></span>\n\t\t\t</div>\n\t\t\t<input type=\"submit\" value=\"@PopForums.Resources.ChangePassword\" class=\"btn btn-primary mb-3\" />\n\t\t</div>\n\t}\n}\nelse\n{\n\t<p>@PopForums.Resources.PasswordResetLinkInvalid <a asp-action=\"Forgot\">@PopForums.Resources.ForgotPasswordQuestion</a></p>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/ResetPasswordSuccess.cshtml",
    "content": "﻿@{\n    ViewBag.Title = PopForums.Resources.PasswordResetSuccess;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.PasswordResetSuccess</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n</ul>\n\n<p>@PopForums.Resources.PasswordResetNote</p>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/Security.cshtml",
    "content": "﻿@model UserEditSecurity\n\n@{\n\tViewBag.Title = PopForums.Resources.Account + \" - \" + PopForums.Resources.ChangeYourEmailPassword;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.Account - @PopForums.Resources.ChangeYourEmailPassword</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item\">@PopForums.Resources.Account</li>\n\t<li class=\"breadcrumb-item dropdown\">\n\t\t<a class=\"dropdown-toggle\" data-bs-toggle=\"dropdown\" href=\"#\" aria-haspopup=\"true\" aria-expanded=\"false\" id=\"SubProfileMenu\">@PopForums.Resources.ChangeYourEmailPassword</a>\n\t\t<div class=\"dropdown-menu\" aria-labelledby=\"SubProfileMenu\">\n\t\t\t<a class=\"dropdown-item\" asp-action=\"EditProfile\">@PopForums.Resources.EditYourProfile</a>\n\t\t\t<a class=\"dropdown-item\" asp-action=\"ManagePhotos\">@PopForums.Resources.ManageYourPhotos</a>\n\t\t\t<a class=\"dropdown-item\" asp-action=\"ExternalLogins\">@PopForums.Resources.ExternalLogins</a>\n\t\t</div>\n\t</li>\n\n</ul>\n\n<h2>@PopForums.Resources.ChangePassword</h2>\n\n<form asp-action=\"ChangePassword\" method=\"post\" role=\"form\">\n\t<div class=\"mb-3\">\n\t\t<label for=\"OldPassword\" class=\"form-label\">@PopForums.Resources.PasswordOld</label>\n\t\t<input type=\"password\" name=\"OldPassword\" class=\"form-control\" />\n\t</div>\n\t<div class=\"mb-3\">\n\t\t<label for=\"NewPassword\" class=\"form-label\">@PopForums.Resources.PasswordNew</label>\n\t\t<input type=\"password\" name=\"NewPassword\" class=\"form-control\" />\n\t</div>\n\t<div class=\"mb-3\">\n\t\t<label for=\"NewPasswordRetype\" class=\"form-label\">@PopForums.Resources.PasswordNewRetype</label>\n\t\t<input type=\"password\" name=\"NewPasswordRetype\" class=\"form-control\" />\n\t</div>\n\t<input type=\"submit\" value=\"@PopForums.Resources.ChangePassword\" class=\"btn btn-primary mb-3\" />\n\t@if (ViewBag.PasswordResult != null)\n\t{<span class=\"alert alert-danger\">@ViewBag.PasswordResult</span>}\n</form>\n\n<hr class=\"my-5\"/>\n\n<h2>@PopForums.Resources.ChangeEmail</h2>\n\n<form asp-action=\"ChangeEmail\" method=\"post\" role=\"form\">\n\t<div class=\"mb-3\">\n\t\t@if (!Model.IsNewUserApproved)\n\t\t{\n\t\t\t<p>@PopForums.Resources.ChangeEmailConsequence</p>\n\t\t}\n\t</div>\n\t<div class=\"mb-3\">\n\t\t<label class=\"form-label\">@PopForums.Resources.Email</label>\n\t\t<p>@Model.OldEmail</p>\n\t</div>\n\t<div class=\"mb-3\">\n\t\t<label for=\"NewEmail\" class=\"form-label\">@PopForums.Resources.EmailNew</label>\n\t\t<input type=\"text\" name=\"NewEmail\" class=\"form-control\" />\n\t</div>\n\t<div class=\"mb-3\">\n\t\t<label for=\"NewEmailRetype\" class=\"form-label\">@PopForums.Resources.EmailNewRetype</label>\n\t\t<input type=\"text\" name=\"NewEmailRetype\" class=\"form-control\" />\n\t</div>\n\t<input type=\"submit\" value=\"@(Model.IsNewUserApproved ? PopForums.Resources.ChangeEmail : PopForums.Resources.ChangeEmailButton)\" class=\"btn btn-primary mb-3\" />\n\t@if (ViewBag.EmailResult != null)\n\t{<span class=\"alert alert-danger\">@ViewBag.EmailResult</span>}\n</form>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/Unsubscribe.cshtml",
    "content": "﻿@{\n    ViewBag.Title = PopForums.Resources.Unsubscribe;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.Unsubscribe</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n</ul>\n\n<p>@PopForums.Resources.UnsubscribeNote</p>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/UnsubscribeFailure.cshtml",
    "content": "﻿@{\n    ViewBag.Title = PopForums.Resources.UnsubscribeFail;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.UnsubscribeFail</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n</ul>\n\n<p>@PopForums.Resources.UnsubscribeLinkBad</p>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/Verify.cshtml",
    "content": "﻿@using PopForums.Configuration\n@inject ISettingsManager SettingsManager\n@{\n\tViewBag.Title = PopForums.Resources.VerifyAccount;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.VerifyAccount</h1>\n@if (!SettingsManager.Current.IsPrivateForumInstance)\n{\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t</ul>\n}\n\n@if (ViewData[\"EmailProblem\"] != null)\n{\n\t<div class=\"alert alert-danger\">@ViewData[\"EmailProblem\"] @ViewData[\"Result\"]</div>\n}\nelse if (ViewData[\"Result\"] != null)\n{\n\t<div class=\"alert alert-success\">@ViewData[\"Result\"]</div>\n}\n\n<div class=\"row\">\n\t<div class=\"col-6\">\n\t\t<form asp-action=\"VerifyCode\" method=\"post\">\n\t\t\t<div role=\"form\">\n\t\t\t\t<div class=\"mb-3\">\n\t\t\t\t\t<label for=\"authorizationCode\" class=\"form-label\">@PopForums.Resources.EnterVerificationCode</label>\n\t\t\t\t\t<input type=\"text\" name=\"authorizationCode\" class=\"form-control\"/>\n\t\t\t\t</div>\n\t\t\t\t<input type=\"submit\" value=\"@PopForums.Resources.VerifyCodeButton\" class=\"btn btn-primary mb-3\"/>\n\t\t\t</div>\n\t\t</form>\n\t</div>\n\t<div class=\"col-6\">\n\t\t<form asp-action=\"RequestCode\" method=\"post\">\n\t\t\t<div role=\"form\">\n\t\t\t\t<div class=\"mb-3\">\n\t\t\t\t\t<label for=\"email\" class=\"form-label\">@PopForums.Resources.VerificationIfYouNeed</label>\n\t\t\t\t\t<input type=\"text\" name=\"email\" class=\"form-control\" />\n\t\t\t\t</div>\n\t\t\t\t<input type=\"submit\" value=\"@PopForums.Resources.SendEmailWithNewCodeButton\" class=\"btn btn-primary mb-3\" />\n\t\t\t</div>\n\t\t</form>\n\t</div>\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/VerifyFail.cshtml",
    "content": "﻿@using PopForums.Configuration\n@inject ISettingsManager SettingsManager\n@{\n\tViewBag.Title = PopForums.Resources.VerificationFailure;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<h1>@PopForums.Resources.VerificationFailure</h1>\n@if (!SettingsManager.Current.IsPrivateForumInstance)\n{\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t</ul>\n}\n\n<p>@PopForums.Resources.VerificationLinkBad <a asp-action=\"Verify\" asp-route-id=\"\">@PopForums.Resources.NeedToVerifyExistingAccount</a></p>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Account/ViewProfile.cshtml",
    "content": "﻿@model DisplayProfile\n\n@{\n\tViewBag.Title = PopForums.Resources.Profile + \" - \" + Model.Name;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n@section HeaderContent {\n\t<script>\n\t\tPopForums.Ready(() => {\n\t\t\tif (window.location.hash && window.location.hash === \"#Awards\") {\n\t\t\t\tconst el = document.querySelector(\"#ProfileTabs #AwardsTabButton\");\n\t\t\t\tnew bootstrap.Tab(el).show();\n\t\t\t}\n\t\t});\n\t</script>\n}\n\n@if (Model.IsApproved)\n{\n\n<h1>\n\t@if (Model.AvatarID.HasValue)\n\t{\n\t\t<img src=\"@Url.Action(\"Avatar\", \"Image\", new { id = Model.AvatarID })\" alt=\"Avatar image\" class=\"img-fluid align-middle me-3 mt-3 mb-3\"/>\n\t}\n\t@Model.Name\n</h1>\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n</ul>\n\n<div id=\"Profile\">\n\t<ul class=\"nav nav-tabs\" role=\"tablist\" id=\"ProfileTabs\">\n\t\t<li class=\"nav-item\"><a class=\"nav-link active\" href=\"#Details\" role=\"tab\" data-bs-toggle=\"tab\">@PopForums.Resources.Profile</a></li>\n\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"#ActivityFeed\" role=\"tab\" data-bs-toggle=\"tab\">@PopForums.Resources.ActivityFeed</a></li>\n\t\t<li class=\"nav-item\"><a class=\"nav-link\" href=\"#Awards\" role=\"tab\" data-bs-toggle=\"tab\" id=\"AwardsTabButton\">@PopForums.Resources.Awards</a></li>\n\t</ul>\n\t<div class=\"tab-content\">\n\t\t<div id=\"Details\" class=\"tab-pane active ps-3\">\n\t\t\t<div>\n\t\t\t\t@if (Model.ShowDetails)\n\t\t\t\t{\n\t\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t\t<div class=\"col-sm-4\">@PopForums.Resources.Contact</div>\n\t\t\t\t\t\t<div class=\"col-sm-8\">\n\t\t\t\t\t\t\t<a asp-controller=\"PrivateMessages\" asp-action=\"Create\" asp-route-id=\"@Model.UserID\">@String.Format(PopForums.Resources.SendNamePM, Model.Name)</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t<div class=\"col-sm-10 offset-sm-4\"><a asp-controller=\"Account\" asp-action=\"Posts\" asp-route-id=\"@Model.UserID\" asp-route-pageNumber=\"\">@(String.Format(PopForums.Resources.NamePosts, Model.Name) + \" (\" + Model.PostCount.ToString(\"N0\") + \")\")</a></div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t<div class=\"col-sm-4\">@PopForums.Resources.Joined</div>\n\t\t\t\t\t<div class=\"col-sm-8\"><pf-formattedtime utctime=\"@Model.Joined.ToString(\"o\")\"></pf-formattedtime></div>\n\t\t\t\t</div>\n\n\t\t\t\t@if (Model.Dob.HasValue)\n\t\t\t\t{\n\t\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t\t<div class=\"col-sm-4\">@PopForums.Resources.Birthday</div>\n\t\t\t\t\t\t<div class=\"col-sm-8\">@Model.Dob.Value.ToString(\"D\")</div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t@if (!String.IsNullOrWhiteSpace(Model.Location))\n\t\t\t\t{\n\t\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t\t<div class=\"col-sm-4\">@PopForums.Resources.Location</div>\n\t\t\t\t\t\t<div class=\"col-sm-8\">@Model.Location</div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t@if (!String.IsNullOrWhiteSpace(Model.Facebook))\n\t\t\t\t{\n\t\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t\t<div class=\"col-sm-4 text-nowrap\"><span class=\"icon icon-facebook\"></span> Facebook</div>\n\t\t\t\t\t\t<div class=\"col-sm-8\"><a href=\"https://facebook.com/@Model.Facebook\" target=\"_blank\">@Model.Facebook</a></div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t@if (!String.IsNullOrWhiteSpace(Model.Instagram))\n\t\t\t\t{\n\t\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t\t<div class=\"col-sm-4 text-nowrap\"><span class=\"icon icon-instagram\"></span> Instagram</div>\n\t\t\t\t\t\t<div class=\"col-sm-8\"><a href=\"https://www.instagram.com/@Model.Instagram\" target=\"_blank\">&#64;@Model.Instagram</a></div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t@if (!String.IsNullOrWhiteSpace(Model.Web))\n\t\t\t\t{\n\t\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t\t<div class=\"col-sm-4\">@PopForums.Resources.Web</div>\n\t\t\t\t\t\t<div class=\"col-sm-8\"><a href=\"@Model.Web\" target=\"_blank\" rel=\"nofollow\">@Model.Web</a></div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t<div class=\"row mb-3\">\n\t\t\t\t\t<div class=\"col-sm-4\">@PopForums.Resources.ScoringGame</div>\n\t\t\t\t\t<div class=\"col-sm-8\">@Model.Points.ToString(\"N0\")</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t@if (Model.ImageID.HasValue && Model.IsImageApproved)\n\t\t\t{\n\t\t\t\t<div class=\"my-3\">\n\t\t\t\t\t<img src=\"@Url.Action(\"UserImage\", \"Image\", new { id = Model.ImageID })\" alt=\"User image\" class=\"img-fluid\"/>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\n\t\t<div id=\"ActivityFeed\" class=\"tab-pane\">\n\t\t\t@foreach (var item in Model.Feed)\n\t\t\t{\n\t\t\t\t<div class=\"alert-light alert mt-2 d-flex\">\n\t\t\t\t\t@if (item.Points > 0)\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"fs-1 fw-bold me-3\">+@item.Points</div>\n\t\t\t\t\t}\n\t\t\t\t\t<div class=\"w-100\">\n\t\t\t\t\t\t<div>@Html.Raw(item.Message)</div>\n\t\t\t\t\t\t<div class=\"text-end small mt-2\"><pf-formattedtime utctime=\"@item.TimeStamp.ToString(\"o\")\"></pf-formattedtime></div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\n\t\t<div id=\"Awards\" class=\"tab-pane\">\n\t\t\t@foreach (var item in Model.UserAwards)\n\t\t\t{\n\t\t\t\t<div class=\"alert-light alert mt-2\">\n\t\t\t\t\t<h2><span class=\"icon icon-trophy-fill\"></span> @item.Title</h2>\n\t\t\t\t\t<p>@item.Description</p>\n\t\t\t\t\t<div class=\"text-end small\"><pf-formattedtime utctime=\"@item.TimeStamp.ToString(\"o\")\"></pf-formattedtime></div>\n\t\t\t\t</div>\n\t\t\t}\n\t\t\t@if (Model.UserAwards.Count == 0)\n\t\t\t{\n\t\t\t\t<div>@PopForums.Resources.None</div>\n\t\t\t}\n\t\t</div>\n\t</div>\n</div>\n\n}\nelse\n{\n\t<h1>@PopForums.Resources.Profile</h1>\n\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t</ul>\n\n\t<p class=\"alert alert-warning\">@PopForums.Resources.AccountNotVerified</p>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Admin/App.cshtml",
    "content": "﻿@inject IConfig Config\n@{\n    Layout = null;\n\tvar isOAuthOnly = Config.IsOAuthOnly;\n}\n\n<!DOCTYPE html>\n\n<html>\n<head>\n\t<meta name=\"viewport\" content=\"width=device-width\" />\n\t<title>@PopForums.Resources.PopForumsAdmin</title>\n\t<environment include=\"Development\">\n\t    <script src=\"~/PopForums/lib/vue/dist/vue.global.js\" asp-append-version=\"true\"></script>\n\t\t<script src=\"~/PopForums/lib/vue-router/dist/vue-router.global.js\" asp-append-version=\"true\"></script>\n    </environment>\n    <environment exclude=\"Development\">\n\t    <script src=\"~/PopForums/lib/vue/dist/vue.global.prod.js\" asp-append-version=\"true\"></script>\n\t\t<script src=\"~/PopForums/lib/vue-router/dist/vue-router.global.prod.js\" asp-append-version=\"true\"></script>\n    </environment>\n\t<script src=\"~/PopForums/lib/axios/dist/axios.min.js\" asp-append-version=\"true\"></script>\n\t<script src=\"~/PopForums/lib/bootstrap/dist/js/bootstrap.bundle.min.js\" asp-append-version=\"true\"></script>\n\t<link href=\"~/PopForums/lib/bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" asp-append-version=\"true\" />\n\t<style>\n\t\t.fade-enter-active, .fade-leave-active {\n\t\t\ttransition: opacity .5s;\n\t\t}\n\t\t.fade-enter, .fade-leave-to {\n\t\t\topacity: 0;\n\t\t}\n\t</style>\n</head>\n<body>\n\t<div class=\"container mb-3\">\n\t\t<div id=\"app\">\n\t\t\t<router-view></router-view>\n\t\t</div>\n\t</div>\n\t<div class=\"container mb-3 small\">&copy;@DateTime.Now.Year, POP World Media, LLC</div>\n\n<script type=\"x-templates\" id=\"Top\">\n\t<main>\n\t\t<div v-show=\"loading\" class=\"spinner-border text-primary float-end me-3 mt-1\" role=\"status\">\n\t\t\t<span class=\"visually-hidden\">Loading...</span>\n\t\t</div>\n\t\t<div class=\"dropdown mb-3\">\n\t\t\t<a class=\"btn btn-secondary dropdown-toggle\" href=\"#\" role=\"button\" id=\"dropdownMenuLink\" data-bs-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n\t\t\t\t@PopForums.Resources.PopForumsAdmin\n\t\t\t</a>\n\t\t\t<div class=\"dropdown-menu\" aria-labelledby=\"dropdownMenuLink\">\n\t\t\t\t<a class=\"dropdown-item\" href=\"/Forums\">@PopForums.Resources.ForumHome</a>\n\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t<router-link to=\"/general\" class=\"dropdown-item\">@PopForums.Resources.GeneralSettings</router-link>\n\t\t\t\t<router-link to=\"/categories\" class=\"dropdown-item\">@PopForums.Resources.Categories</router-link>\n\t\t\t\t<router-link to=\"/forums\" class=\"dropdown-item\">@PopForums.Resources.Forums</router-link>\n\t\t\t\t<router-link to=\"/forumpermissions\" class=\"dropdown-item\">@PopForums.Resources.ForumPermisions</router-link>\n\t\t\t\t@if (!isOAuthOnly)\n\t\t\t\t{\n\t\t\t\t\t<router-link to=\"/email\" class=\"dropdown-item\">@PopForums.Resources.Email</router-link>\n\t\t\t\t}\n\t\t\t\t<router-link to=\"/search\" class=\"dropdown-item\">@PopForums.Resources.Search</router-link>\n\t\t\t\t@if (!isOAuthOnly)\n\t\t\t\t{\n\t\t\t\t\t<router-link to=\"/externallogins\" class=\"dropdown-item\">@PopForums.Resources.ExternalLogins</router-link>\n\t\t\t\t}\n\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t<router-link to=\"/recentusers\" class=\"dropdown-item\">Recent Users</router-link>\n\t\t\t\t<router-link to=\"/edituser\" class=\"dropdown-item\">@PopForums.Resources.EditUser</router-link>\n\t\t\t\t@if (!isOAuthOnly)\n\t\t\t\t{\n\t\t\t\t\t<router-link to=\"/userroles\" class=\"dropdown-item\">@PopForums.Resources.UserRoles</router-link>\n\t\t\t\t}\n\t\t\t\t<router-link to=\"/userimageapproval\" class=\"dropdown-item\">@PopForums.Resources.UserImageApproval</router-link>\n\t\t\t\t@if (!isOAuthOnly)\n\t\t\t\t{\n\t\t\t\t\t<router-link to=\"/emailipban\" class=\"dropdown-item\">@PopForums.Resources.EmailIpBan</router-link>\n\t\t\t\t\t<router-link to=\"/emailusers\" class=\"dropdown-item\">@PopForums.Resources.EmailUsers</router-link>\n\t\t\t\t}\n\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t<router-link to=\"/scoringgame\" class=\"dropdown-item\">@PopForums.Resources.ScoringGame</router-link>\n\t\t\t\t<router-link to=\"/eventdefinitions\" class=\"dropdown-item\">@PopForums.Resources.EventDefinitions</router-link>\n\t\t\t\t<router-link to=\"/awarddefinitions\" class=\"dropdown-item\">@PopForums.Resources.AwardDefinitions</router-link>\n\t\t\t\t<router-link to=\"/manualevent\" class=\"dropdown-item\">@PopForums.Resources.ManualEvent</router-link>\n\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t<router-link to=\"/iphistory\" class=\"dropdown-item\">@PopForums.Resources.IpHistory</router-link>\n\t\t\t\t<router-link to=\"/securitylog\" class=\"dropdown-item\">@PopForums.Resources.SecurityLog</router-link>\n\t\t\t\t<router-link to=\"/moderationlog\" class=\"dropdown-item\">@PopForums.Resources.ModerationLog</router-link>\n\t\t\t\t<router-link to=\"/errorlog\" class=\"dropdown-item\">@PopForums.Resources.ErrorLog</router-link>\n\t\t\t\t<router-link to=\"/services\" class=\"dropdown-item\">@PopForums.Resources.Services</router-link>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<router-view v-on:setLoading=\"setLoading($event)\" v-bind:loading=\"loading\"></router-view>\n\t</main>\n\t</script>\n\n<script type=\"x-templates\" id=\"General\">\n\t<main>\n\t\t<h2>@PopForums.Resources.GeneralSettings</h2>\n\t\t<div>\n\t\t\t<h3>@PopForums.Resources.ForumSettings</h3>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"ForumTitle\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.Title</label>\n\t\t\t\t<div class=\"col-sm-8\"><input type=\"text\" v-model=\"settings.forumTitle\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"TopicsPerPage\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.TopicsPerPage</label>\n\t\t\t\t<div class=\"col-sm-2\"><input type=\"text\" v-model=\"settings.topicsPerPage\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"PostsPerPage\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.PostsPerPage</label>\n\t\t\t\t<div class=\"col-sm-2\"><input type=\"text\" v-model=\"settings.postsPerPage\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"SessionLength\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.SessionLength</label>\n\t\t\t\t<div class=\"col-sm-2\"><input type=\"text\" v-model=\"settings.sessionLength\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"MinimumSecondsBetweenPosts\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.MinimumTimeBetweenPosts</label>\n\t\t\t\t<div class=\"col-sm-2\"><input type=\"text\" v-model=\"settings.minimumSecondsBetweenPosts\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t@if (!isOAuthOnly)\n\t\t\t{\n\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t\t\t<label for=\"IsNewUserApproved\"><input type=\"checkbox\" v-model=\"settings.isNewUserApproved\" id=\"IsNewUserApproved\" class=\"form-check-input\" /> @PopForums.Resources.NewUserApprovedWithoutVerification</label>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t}\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t\t<label for=\"IsNewUserImageApproved\"><input type=\"checkbox\" v-model=\"settings.isNewUserImageApproved\" id=\"IsNewUserImageApproved\" class=\"form-check-input\" /> @PopForums.Resources.NewUserImageApprovedWithoutMod</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-sm-4 col-form-label\">@PopForums.Resources.UserImageMaxDim</label>\n\t\t\t\t<div class=\"input-group col\">\n\t\t\t\t\t<input type=\"text\" v-model=\"settings.userImageMaxHeight\" class=\"form-control\" />\n\t\t\t\t\t<span class=\"input-group-text\">H</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"input-group col\">\n\t\t\t\t\t<input type=\"text\" v-model=\"settings.userImageMaxWidth\" class=\"form-control\" />\n\t\t\t\t\t<span class=\"input-group-text\">W</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"offset-sm-4 col-sm-4\">\n\t\t\t\t\t<div class=\"input-group\">\n\t\t\t\t\t\t<input type=\"text\" v-model=\"settings.userImageMaxkBytes\" class=\"form-control\" placeholder=\"@PopForums.Resources.Size\" />\n\t\t\t\t\t\t<span class=\"input-group-text\">kB</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-sm-4 control-label\">@PopForums.Resources.UserAvatarMaxDim</label>\n\t\t\t\t<div class=\"input-group col\">\n\t\t\t\t\t<input type=\"text\" v-model=\"settings.userAvatarMaxHeight\" class=\"form-control\" />\n\t\t\t\t\t<span class=\"input-group-text\">H</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"input-group col\">\n\t\t\t\t\t<input type=\"text\" v-model=\"settings.userAvatarMaxWidth\" class=\"form-control\" />\n\t\t\t\t\t<span class=\"input-group-text\">W</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"offset-sm-4 col-sm-4\">\n\t\t\t\t\t<div class=\"input-group\">\n\t\t\t\t\t\t<input type=\"text\" v-model=\"settings.userAvatarMaxkBytes\" class=\"form-control\" placeholder=\"@PopForums.Resources.Size\" />\n\t\t\t\t\t\t<span class=\"input-group-text\">kB</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-sm-4 control-label\">@PopForums.Resources.UploadImage</label>\n\t\t\t\t<div class=\"input-group col\">\n\t\t\t\t\t<input type=\"text\" v-model=\"settings.postImageMaxHeight\" class=\"form-control\" />\n\t\t\t\t\t<span class=\"input-group-text\">H</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"input-group col\">\n\t\t\t\t\t<input type=\"text\" v-model=\"settings.postImageMaxWidth\" class=\"form-control\" />\n\t\t\t\t\t<span class=\"input-group-text\">W</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"offset-sm-4 col-sm-4\">\n\t\t\t\t\t<div class=\"input-group\">\n\t\t\t\t\t\t<input type=\"text\" v-model=\"settings.postImageMaxkBytes\" class=\"form-control\" placeholder=\"@PopForums.Resources.Size\" />\n\t\t\t\t\t\t<span class=\"input-group-text\">kB</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-sm-4 control-label\">YouTube</label>\n\t\t\t\t<div class=\"input-group col\">\n\t\t\t\t\t<input type=\"text\" v-model=\"settings.youTubeHeight\" class=\"form-control\" />\n\t\t\t\t\t<div class=\"input-group-text\">H</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"input-group col\">\n\t\t\t\t\t<input type=\"text\" v-model=\"settings.youTubeWidth\" class=\"form-control\" />\n\t\t\t\t\t<div class=\"input-group-text\">W</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t\t<label for=\"CloseOldTopics\"><input type=\"checkbox\" v-model=\"settings.isClosingAgedTopics\" id=\"CloseOldTopics\" class=\"form-check-input\" /> @PopForums.Resources.CloseOldTopics</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"col-sm-4 offset-sm-4\">\n\t\t\t\t\t<div class=\"input-group\">\n\t\t\t\t\t\t<input type=\"text\" v-model=\"settings.closeAgedTopicsDays\" class=\"form-control\" />\n\t\t\t\t\t\t<div class=\"input-group-text\">@PopForums.Resources.Days</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t@if (!isOAuthOnly)\n\t\t\t{\n\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t\t\t<label for=\"IsPrivateForumInstance\"><input type=\"checkbox\" v-model=\"settings.isPrivateForumInstance\" id=\"IsPrivateForumInstance\" class=\"form-check-input\" /> @PopForums.Resources.PrivateForum</label>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t}\n\n\t\t\t<h3>@PopForums.Resources.Parsing</h3>\n\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"CensorWords\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.CensorWords</label>\n\t\t\t\t<div class=\"col-sm-8\"><textarea class=\"form-control\" rows=\"3\" v-model=\"settings.censorWords\"></textarea></div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"CensorCharacter\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.CensorReplacementChar</label>\n\t\t\t\t<div class=\"col-sm-3\"><input type=\"text\" v-model=\"settings.censorCharacter\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t\t<label for=\"AllowImages\"><input type=\"checkbox\" v-model=\"settings.allowImages\" id=\"AllowImages\" class=\"form-check-input\" /> @PopForums.Resources.AllowImages / YouTube</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<h3>@PopForums.Resources.Logging</h3>\n\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t\t<label for=\"LogSecurity\"><input type=\"checkbox\" v-model=\"settings.logSecurity\" id=\"LogSecurity\" class=\"form-check-input\" /> @PopForums.Resources.LogSecurity</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t\t<label for=\"LogModeration\"><input type=\"checkbox\" v-model=\"settings.logModeration\" id=\"LogModeration\" class=\"form-check-input\" /> @PopForums.Resources.LogMod</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t\t<label for=\"LogErrors\"><input type=\"checkbox\" v-model=\"settings.logErrors\" id=\"LogErrors\" class=\"form-check-input\" /> @PopForums.Resources.LogErrors</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t@if (!isOAuthOnly)\n\t\t\t{\n\t\t\t\t<h3>@PopForums.Resources.TermsOfService</h3>\n\n\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t<div class=\"col-sm-12\"><textarea class=\"form-control\" rows=\"6\" v-model=\"settings.termsOfService\"></textarea></div>\n\t\t\t\t</div>\n\t\t\t}\n\n\t\t\t<input type=\"button\" v-on:click=\"save('@PopForums.Resources.SettingsSaved')\" :disabled=\"loading\" value=\"@PopForums.Resources.Save\" class=\"btn btn-primary\" />\n\t\t\t<transition name=\"fade\"><span v-show=\"alert\" class=\"alert alert-success ms-3\">{{message}}</span></transition>\n\n\t\t</div>\n\t</main>\n\t</script>\n\n<script type=\"x-templates\" id=\"Categories\">\n\t<main>\n\t\t<h2>@PopForums.Resources.Categories</h2>\n\t\t<div class=\"form-inline\">\n\t\t\t<input type=\"text\" v-model=\"newCategory\" class=\"form-control mb-3\" />\n\t\t\t<input type=\"button\" v-on:click=\"saveNew\" :disabled=\"loading\" value=\"@PopForums.Resources.AddNew\" class=\"btn btn-primary mb-3\" />\n\t\t</div>\n\t\t<table class=\"table table-sm mt-3\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th colspan=\"2\">@PopForums.Resources.Move</th>\n\t\t\t\t\t<th>@PopForums.Resources.Category</th>\n\t\t\t\t\t<th>&nbsp;</th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t\t<tr v-for=\"(category, index) in categories\">\n\t\t\t\t\t<td class=\"text-right\"><input type=\"button\" value=\"@PopForums.Resources.Up\" class=\"btn btn-secondary ms-3\" v-on:click=\"up(category)\" v-if=\"index != 0\" style=\"width:5em;\" /></td>\n\t\t\t\t\t<td class=\"text-left\"><input type=\"button\" value=\"@PopForums.Resources.Down\" class=\"btn btn-secondary ms-3\" v-on:click=\"down(category)\" v-if=\"index != categories.length-1\" style=\"width:5em;\" /></td>\n\t\t\t\t\t<td>{{category.title}}</td>\n\t\t\t\t\t<td class=\"text-nowrap text-right\">\n\t\t\t\t\t\t<input type=\"button\" value=\"@PopForums.Resources.Edit\" class=\"btn btn-primary ms-3\" v-on:click=\"editCat(category)\" />\n\t\t\t\t\t\t<input type=\"button\" value=\"@PopForums.Resources.Delete\" class=\"btn btn-primary ms-3\" v-on:click=\"deleteCat(category)\" />\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\t\t<div class=\"modal\" tabindex=\"-1\" role=\"dialog\" ref=\"modal\">\n\t\t\t<div class=\"modal-dialog\" role=\"document\">\n\t\t\t\t<div class=\"modal-content\">\n\t\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t\t<h5 class=\"modal-title\">@PopForums.Resources.Edit</h5>\n\t\t\t\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t\t<input type=\"text\" v-model=\"editCategory\" class=\"form-control\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-footer\">\n\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-primary\" v-on:click=\"saveCat\" data-bs-dismiss=\"modal\">@PopForums.Resources.Save</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"Forums\">\n\t<main>\n\t\t<h2>@PopForums.Resources.Forums</h2>\n\n\t\t<p><input type=\"button\" class=\"btn btn-primary\" v-on:click=\"newForum\" value=\"@PopForums.Resources.AddForum\" /></p>\n\n\t\t<table class=\"table table-sm mt-3\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th colspan=\"2\">@PopForums.Resources.Move</th>\n\t\t\t\t\t<th>@PopForums.Resources.Forum</th>\n\t\t\t\t\t<th>@PopForums.Resources.Visible</th>\n\t\t\t\t\t<th>@PopForums.Resources.Archived</th>\n\t\t\t\t\t<th>Q&amp;A</th>\n\t\t\t\t\t<th>&nbsp;</th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t\t<template v-for=\"category in categories\">\n\t\t\t\t\t<tr class=\"bg-primary text-white\"><td colspan=\"7\">{{category.category.title}}</td></tr>\n\t\t\t\t\t<tr v-for=\"(forum, index) in category.forums\">\n\t\t\t\t\t\t<td class=\"text-right\"><input type=\"button\" value=\"@PopForums.Resources.Up\" class=\"btn btn-secondary ms-3\" v-on:click=\"up(forum)\" v-if=\"index != 0\" style=\"width:5em;\" /></td>\n\t\t\t\t\t\t<td class=\"text-left\"><input type=\"button\" value=\"@PopForums.Resources.Down\" class=\"btn btn-secondary ms-3\" v-on:click=\"down(forum)\" v-if=\"index != category.forums.length-1\" style=\"width:5em;\" /></td>\n\t\t\t\t\t\t<td>{{forum.title}}</td>\n\t\t\t\t\t\t<td><input type=\"checkbox\" v-model=\"forum.isVisible\" disabled class=\"form-check-input\" /></td>\n\t\t\t\t\t\t<td><input type=\"checkbox\" v-model=\"forum.isArchived\" disabled class=\"form-check-input\" /></td>\n\t\t\t\t\t\t<td><input type=\"checkbox\" v-model=\"forum.isQAForum\" disabled class=\"form-check-input\" /></td>\n\t\t\t\t\t\t<td><input type=\"button\" value=\"@PopForums.Resources.Edit\" class=\"btn btn-primary ms-3\" v-on:click=\"editForum(forum)\" /></td>\n\t\t\t\t\t</tr>\n\t\t\t\t</template>\n\t\t\t</tbody>\n\t\t</table>\n\t\t<div class=\"modal\" tabindex=\"-1\" role=\"dialog\" ref=\"modal\">\n\t\t\t<div class=\"modal-dialog\" role=\"document\">\n\t\t\t\t<div class=\"modal-content\">\n\t\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t\t<h5 class=\"modal-title\">@PopForums.Resources.Edit</h5>\n\t\t\t\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"title\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.Title</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"editingForum.title\" class=\"form-control\" id=\"title\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"description\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.Description</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"editingForum.description\" class=\"form-control\" id=\"description\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"categoryID\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.Category</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t\t\t\t\t<select id=\"categoryID\" v-model=\"editingForum.categoryID\" class=\"form-select\">\n\t\t\t\t\t\t\t\t\t<option v-for=\"c in categories\" v-bind:value=\"c.category.categoryID\">{{c.category.title}}</option>\n\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<div class=\"col-sm-10 offset-sm-2\">\n\t\t\t\t\t\t\t\t<label for=\"IsVisible\"><input type=\"checkbox\" v-model=\"editingForum.isVisible\" id=\"IsVisible\" class=\"form-check-input\" /> @PopForums.Resources.Visible</label>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<div class=\"col-sm-10 offset-sm-2\">\n\t\t\t\t\t\t\t\t<label for=\"IsArchived\"><input type=\"checkbox\" v-model=\"editingForum.isArchived\" id=\"IsArchived\" class=\"form-check-input\" /> @PopForums.Resources.Archived</label>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<div class=\"col-sm-10 offset-sm-2\">\n\t\t\t\t\t\t\t\t<label for=\"IsQAForum\"><input type=\"checkbox\" v-model=\"editingForum.isQAForum\" id=\"IsQAForum\" class=\"form-check-input\" /> Q&amp;A @PopForums.Resources.Forum</label>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<div class=\"col-sm-12\">\n\t\t\t\t\t\t\t\t<label for=\"forumAdapterName\" class=\"col-form-label\">@PopForums.Resources.ForumAdapter</label>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<div class=\"col-sm-12\"><input type=\"text\" v-model=\"editingForum.forumAdapterName\" class=\"form-control\" id=\"forumAdapterName\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-footer\">\n\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-primary\" v-on:click=\"saveForum\" data-bs-dismiss=\"modal\">@PopForums.Resources.Save</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"ForumPermissions\">\n\t<main>\n\t\t<h2>@PopForums.Resources.ForumPermisions</h2>\n\n\t\t<div class=\"mb-3\">\n\t\t\t<label>@PopForums.Resources.Forum</label>\n\t\t\t<select id=\"ForumSelect\" class=\"form-select\" v-model=\"selectedForum\" v-on:change=\"forumChange\">\n\t\t\t\t<optgroup v-for=\"c in categories\" v-bind:label=\"c.category.title\">\n\t\t\t\t\t<option v-for=\"f in c.forums\" v-bind:value=\"f.forumID\">{{f.title}}</option>\n\t\t\t\t</optgroup>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<div class=\"container-fluid mb-3\">\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-3\">@PopForums.Resources.PostingRoles</div>\n\t\t\t\t<div class=\"offset-5 col-3\">@PopForums.Resources.ViewingRoles</div>\n\t\t\t</div>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-3\"><select size=\"6\" class=\"form-select\" v-model=\"selectedPost\"><option v-for=\"r in postRoles\">{{r}}</option></select></div>\n\t\t\t\t<div class=\"col-1\">\n\t\t\t\t\t<input type=\"button\" value=\"&lt;&lt;\" class=\"btn btn-primary btn-sm mb-3\" v-on:click=\"modify('addPost', selectedAll)\" /><br />\n\t\t\t\t\t<input type=\"button\" value=\"&gt;&gt;\" class=\"btn btn-primary btn-sm\" v-on:click=\"modify('removePost', selectedPost)\" />\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-3\"><select size=\"6\" class=\"form-select\" v-model=\"selectedAll\"><option v-for=\"r in allRoles\">{{r}}</option></select></div>\n\t\t\t\t<div class=\"col-1\">\n\t\t\t\t\t<input type=\"button\" value=\"&gt;&gt;\" class=\"btn btn-primary btn-sm mb-3\" v-on:click=\"modify('addView', selectedAll)\" /><br />\n\t\t\t\t\t<input type=\"button\" value=\"&lt;&lt;\" class=\"btn btn-primary btn-sm\" v-on:click=\"modify('removeView', selectedView)\" />\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-3\"><select size=\"6\" class=\"form-select\" v-model=\"selectedView\"><option v-for=\"r in viewRoles\">{{r}}</option></select></div>\n\t\t\t</div>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-3\"><input type=\"button\" value=\"@PopForums.Resources.RemoveAll\" class=\"btn btn-primary mt-3\" v-on:click=\"modify('removeAllPost', null)\" /></div>\n\t\t\t\t<div class=\"offset-5 col-3\"><input type=\"button\" value=\"@PopForums.Resources.RemoveAll\" class=\"btn btn-primary mt-3\" v-on:click=\"modify('removeAllView', null)\" /></div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<p>@PopForums.Resources.ForumPermissionInstructions</p>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"Email\">\n\t<main>\n\t\t<h2>@PopForums.Resources.Email</h2>\n\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"SmtpServer\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.SmtpServer</label>\n\t\t\t<div class=\"col-sm-8\"><input type=\"text\" v-model=\"settings.smtpServer\" id=\"SmtpServer\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"SmtpPort\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.SmtpPort</label>\n\t\t\t<div class=\"col-sm-3\"><input type=\"text\" v-model=\"settings.smtpPort\" id=\"SmtpPort\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"MailerAddress\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.FromEmailAddress</label>\n\t\t\t<div class=\"col-sm-8\"><input type=\"text\" v-model=\"settings.mailerAddress\" id=\"MailerAddress\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"ReplyToAddress\" class=\"col-sm-4 col-form-label\">Reply To</label>\n\t\t\t<div class=\"col-sm-8\"><input type=\"text\" v-model=\"settings.replyToAddress\" id=\"ReplyToAddress\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t<label for=\"UseSslSmtp\"><input type=\"checkbox\" v-model=\"settings.useSslSmtp\" id=\"UseSslSmtp\" class=\"form-check-input\" /> @PopForums.Resources.UseSsl</label>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"col-sm-8 offset-sm-4\">\n\t\t\t\t<label for=\"UseEsmtp\"><input type=\"checkbox\" v-model=\"settings.useEsmtp\" id=\"UseEsmtp\" class=\"form-check-input\" /> @PopForums.Resources.UseEsmtpCred</label>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"SmtpUser\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.SmtpUser</label>\n\t\t\t<div class=\"col-sm-8\"><input type=\"text\" v-model=\"settings.smtpUser\" id=\"SmtpUser\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"SmtpPassword\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.SmtpPassword</label>\n\t\t\t<div class=\"col-sm-8\"><input type=\"text\" v-model=\"settings.smtpPassword\" id=\"SmtpPassword\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"MailSendingInverval\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.SendingInterval</label>\n\t\t\t<div class=\"col-sm-3\"><input type=\"text\" v-model=\"settings.mailSendingInverval\" id=\"MailSendingInverval\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"MailerQuantity\" class=\"col-sm-4 col-form-label\">Mailer quantity</label>\n\t\t\t<div class=\"col-sm-3\"><input type=\"text\" v-model=\"settings.mailerQuantity\" id=\"MailerQuantity\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label class=\"col-sm-4 col-form-label\">@PopForums.Resources.Signature</label>\n\t\t\t<div class=\"col-sm-8\">\n\t\t\t\t<textarea v-model=\"settings.mailSignature\" class=\"form-control\" rows=\"4\"></textarea>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<input type=\"button\" v-on:click=\"save\" :disabled=\"loading\" value=\"@PopForums.Resources.Save\" class=\"btn btn-primary\" />\n\t\t<transition name=\"fade\"><span v-show=\"alert\" class=\"alert alert-success ms-3\"> @PopForums.Resources.SettingsSaved </span></transition>\n\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"Search\">\n\t<main>\n\t\t<h2>@PopForums.Resources.Search</h2>\n\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"SearchIndexingInterval\" class=\"col-sm-4 col-form-label\">@PopForums.Resources.Millseconds</label>\n\t\t\t<div class=\"col-sm-8\"><input type=\"text\" v-model=\"settings.searchIndexingInterval\" id=\"SearchIndexingInterval\" class=\"form-control\" /></div>\n\t\t</div>\n\t\t<input type=\"button\" v-on:click=\"save\" :disabled=\"loading\" value=\"@PopForums.Resources.Save\" class=\"btn btn-primary\" />\n\t\t<transition name=\"fade\"><span v-show=\"alert\" class=\"alert alert-success ms-3\">@PopForums.Resources.SettingsSaved</span></transition>\n        \n        <hr/>\n\n\t\t<h3 class=\"mt-3\">@PopForums.Resources.JunkWords</h3>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"col-sm-8\"><input type=\"text\" v-model=\"newWord\" class=\"form-control\" /></div>\n\t\t\t<div class=\"col-sm-3\"><input type=\"button\" v-on:click=\"createWord\" value=\"@PopForums.Resources.CreateJunkWordButton\" class=\"btn btn-primary\" /></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n            <div class=\"col-sm-12\">\n\t\t\t\t<select size=\"10\" class=\"form-select\" v-model=\"selectedWord\">\n\t\t\t\t\t<option v-for=\"w in junkWords\">{{w}}</option>\n\t\t\t\t</select>\n            </div>\n\t\t</div>\n\t\t<input type=\"button\" v-on:click=\"deleteWord\" value=\"@PopForums.Resources.DeleteJunkWordButton\" class=\"btn btn-primary\" />\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"ExternalLogins\">\n\t<main>\n\t\t<h2>@PopForums.Resources.ExternalLogins</h2>\n\t\t<p>(registered redirect URI is https://yourdomain/Forums/Identity/CallbackHandler)</p>\n\n\t\t<div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\"><b>Google</b></label>\n\t\t\t\t<div class=\"col-sm-10 mt-2\"><input type=\"checkbox\" v-model=\"settings.useGoogleLogin\" class=\"form-check-input\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">ClientId:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.googleClientId\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">ClientSecret:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.googleClientSecret\" class=\"form-control\" /></div>\n\t\t\t</div>\n            <hr/>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\"><b>Facebook</b></label>\n\t\t\t\t<div class=\"col-sm-10 mt-2\"><input type=\"checkbox\" v-model=\"settings.useFacebookLogin\" class=\"form-check-input\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">AppID:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.facebookAppID\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">AppSecret:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.facebookAppSecret\" class=\"form-control\" /></div>\n\t\t\t</div>\n            <hr/>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\"><b>Microsoft</b></label>\n\t\t\t\t<div class=\"col-sm-10 mt-2\"><input type=\"checkbox\" v-model=\"settings.useMicrosoftLogin\" class=\"form-check-input\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">ClientID:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.microsoftClientID\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">ClientSecret:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.microsoftClientSecret\" class=\"form-control\" /></div>\n\t\t\t</div>\n            <hr/>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\"><b>OAuth2 returning JWT</b></label>\n\t\t\t\t<div class=\"col-sm-10 mt-2\"><input type=\"checkbox\" v-model=\"settings.useOAuth2Login\" class=\"form-check-input\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">Client ID:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.oAuth2ClientID\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">Client Secret:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.oAuth2ClientSecret\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">Base login URL:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.oAuth2LoginUrl\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">Token URL:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.oAuth2TokenUrl\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">Display name:</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.oAuth2DisplayName\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t\t<div class=\"row mb-3\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">Scope (separate by space):</label>\n\t\t\t\t<div class=\"col-sm-10\"><input type=\"text\" v-model=\"settings.oAuth2Scope\" class=\"form-control\" /></div>\n\t\t\t</div>\n\t\t</div>\n\n\n\t\t<input type=\"button\" v-on:click=\"save('@PopForums.Resources.SettingsSaved')\" :disabled=\"loading\" value=\"@PopForums.Resources.Save\" class=\"btn btn-primary\" />\n\t\t<transition name=\"fade\"><span v-show=\"alert\" class=\"alert alert-success ms-3\">{{message}}</span></transition>\n\t</main>\n</script>\n\n\n<script type=\"x-templates\" id=\"RecentUsers\">\n\t<main>\n        <h2>Recent Users</h2>\n        \n        <table class=\"table table-striped mt-3\">\n            <tbody>\n\t            <tr v-for=\"u in results\">\n\t                <td><router-link :to=\"{path:'/edituser/'+u.userID}\">{{u.name}}</router-link></td>\n\t                <td>{{u.email}}</td>\n\t                <td>{{u.creationDate}}</td>\n                    <td v-html=\"u.ip\"></td>\n\t            </tr>\n            </tbody>\n        </table>\n    </main>\n</script>\n\n\n<script type=\"x-templates\" id=\"EditUser\">\n\t<main>\n\t\t<h2>@PopForums.Resources.EditUser</h2>\n\n\t\t<form v-on:submit.prevent=\"search\">\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"mb-3 col-sm-8\"><input type=\"text\" v-model=\"searchText\" class=\"form-control\" /></div>\n\t\t\t</div>\n            <div class=\"row mb-3\">\n                <div>\n\t\t\t\t\t<div class=\"form-check form-check-inline\">\n\t\t\t\t\t\t<input class=\"form-check-input\" type=\"radio\" value=\"name\" id=\"SearchName\" v-model=\"searchType\">\n\t\t\t\t\t\t<label class=\"form-check-label\" for=\"SearchName\">Name</label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"form-check form-check-inline\">\n\t\t\t\t\t\t<input class=\"form-check-input\" type=\"radio\" value=\"email\" id=\"SearchEmail\" v-model=\"searchType\">\n\t\t\t\t\t\t<label class=\"form-check-label\" for=\"SearchEmail\">Email</label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"form-check form-check-inline\">\n\t\t\t\t\t\t<input class=\"form-check-input\" type=\"radio\" value=\"role\" id=\"SearchRole\" v-model=\"searchType\">\n\t\t\t\t\t\t<label class=\"form-check-label\" for=\"SearchRole\">Role</label>\n\t\t\t\t\t</div>\n                </div>\n            </div>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"mb-3\"><input type=\"button\" v-on:click=\"search\" value=\"@PopForums.Resources.Search\" class=\"btn btn-primary\" /></div>\n\t\t\t</div>\n\t\t</form>\n\t\t\t<transition name=\"fade\"><span v-show=\"searchAlert\" class=\"alert alert-warning ms-3\">@PopForums.Resources.NoResults</span></transition>\n\t\t\t<div v-for=\"u in searchResults\"><router-link :to=\"{path:'/edituser/'+u.userID}\">{{u.name}}</router-link> - {{u.email}}</div>\n\t</main>\n\n\n</script>\n\n<script type=\"x-templates\" id=\"EditUserDetail\">\n\t<main>\n\t\t<h2>@PopForums.Resources.EditUser</h2>\n\n\t\t@if (!isOAuthOnly)\n\t\t{\n\t\t\t<div class=\"row\">\n\t\t\t\t<h2>@PopForums.Resources.Security</h2>\n\t\t\t</div>\n\t\t}\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label class=\"col-sm-2 col-form-label\">@PopForums.Resources.Name</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<p class=\"form-control-static\">{{user.name}}</p>\n\t\t\t</div>\n\t\t</div>\n\t\t@if (!isOAuthOnly)\n\t\t{\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-sm-2 col-form-label\">@PopForums.Resources.Email</label>\n\t\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t\t<p class=\"form-control-static\"><a v-bind:href=\"'mailto:' + user.email\">{{user.email}}</a></p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"NewEmail\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.EmailNewOptional</label>\n\t\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t\t<input type=\"email\" class=\"form-control\" autocomplete=\"new-email\" v-model=\"user.newEmail\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label for=\"NewPassword\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.PasswordNewOptional</label>\n\t\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t\t<input type=\"password\" class=\"form-control\" autocomplete=\"new-password\" v-model=\"user.newPassword\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" id=\"IsApproved\" v-model=\"user.isApproved\" class=\"form-check-input me-2\" /> \n\t\t\t\t\t<label class=\"form-check-label\" for=\"IsApproved\"> @PopForums.Resources.IsApproved</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t}\n\n\t\t<div class=\"row\">\n\t\t\t<h2>@PopForums.Resources.Options</h2>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" id=\"IsSubscribed\" v-model=\"user.isSubscribed\" class=\"form-check-input me-2\" /> \n\t\t\t\t<label class=\"form-check-label\" for=\"IsSubscribed\"> @PopForums.Resources.SubscribeToList</label>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" id=\"IsAutoFollowOnReply\" v-model=\"user.isAutoFollowOnReply\" class=\"form-check-input me-2\" /> \n\t\t\t\t<label class=\"form-check-label\" for=\"IsAutoFollowOnReply\"> @PopForums.Resources.AutoFollow</label>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" id=\"ShowDetails\" v-model=\"user.showDetails\" class=\"form-check-input me-2\" /> \n\t\t\t\t<label class=\"form-check-label\" for=\"ShowDetails\"> @PopForums.Resources.ShowProfileDetails</label>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" id=\"IsPlainText\" v-model=\"user.isPlainText\" class=\"form-check-input me-2\" /> \n\t\t\t\t<label class=\"form-check-label\" for=\"IsPlainText\"> @PopForums.Resources.ForcePlainTextBox</label>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<div class=\"offset-sm-2 col-sm-10\">\n\t\t\t\t<input class=\"form-check-input\" type=\"checkbox\" id=\"HideVanity\" v-model=\"user.hideVanity\" class=\"form-check-input me-2\" /> \n\t\t\t\t<label class=\"form-check-label\" for=\"HideVanity\"> @PopForums.Resources.HideVanity</label>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"row\">\n\t\t\t<h2>@PopForums.Resources.Details</h2>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"Signature\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.Signature</label>\n\t\t\t<div class=\"col-sm-10\"><textarea id=\"Signature\" class=\"form-control\" rows=\"3\" v-model=\"user.signature\"></textarea></div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"Location\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.Location</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<input type=\"text\" id=\"Location\" class=\"form-control\" v-model=\"user.location\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"Dob\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.DateOfBirth</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<input type=\"text\" id=\"Dob\" class=\"form-control\" v-model=\"user.dob\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"Facebook\" class=\"col-sm-2 col-form-label\">Facebook</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<div class=\"input-group\">\n\t\t\t\t\t<div class=\"input-group-text\">https://facebook.com/</div>\n\t\t\t\t\t<input type=\"text\" id=\"Facebook\" class=\"form-control\" v-model=\"user.facebook\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"Instagram\" class=\"col-sm-2 col-form-label\">Instagram</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<div class=\"input-group\">\n\t\t\t\t\t<div class=\"input-group-text\">&#64;</div>\n\t\t\t\t\t<input type=\"text\" id=\"Instagram\" class=\"form-control\" v-model=\"user.instagram\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"mb-3 row\">\n\t\t\t<label for=\"Web\" class=\"col-sm-2 col-form-label\">@PopForums.Resources.Web</label>\n\t\t\t<div class=\"col-sm-10\">\n\t\t\t\t<input type=\"text\" id=\"Web\" class=\"form-control\" v-model=\"user.web\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div v-show=\"@((!isOAuthOnly).ToString().ToLower())\">\n\t\t\t<div class=\"row\">\n\t\t\t\t<h2>@PopForums.Resources.Roles</h2>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<div class=\"col-sm-10 offset-sm-2\">\n\t\t\t\t\t<template v-for=\"r in roles\">\n\t\t\t\t\t\t<input :id=\"'rz' + r\" type=\"checkbox\" name=\"roles\" :value=\"r\" v-model=\"user.roles\" class=\"form-check-input me-2\" /> \n\t\t\t\t\t\t<label :for=\"'rz' + r\"> {{r}}</label>\n\t\t\t\t\t\t<br />\n\t\t\t\t\t</template>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<input type=\"button\" class=\"btn btn-primary\" value=\"@PopForums.Resources.Save\" :disabled=\"loading\" v-on:click=\"saveUser('@PopForums.Resources.SettingsSaved')\" />\n\t\t<transition name=\"fade\"><span v-show=\"alert\" class=\"alert alert-success ms-3\">{{message}}</span></transition>\n\n\t\t<hr />\n\t\t<h3>@PopForums.Resources.Avatar</h3>\n\t\t<input type=\"file\" :disabled=\"loading\" v-on:change=\"uploadAvatar($event.target)\" accept=\"image/*\" class=\"form-control mb-3\">\n\t\t<div><img v-if=\"user.avatarID\" v-bind:src=\"'/Forums/Image/Avatar/' + user.avatarID\" /></div>\n\t\t<p><input type=\"button\" class=\"btn btn-primary mt-3\" v-on:click=\"removeAvatar\" v-show=\"user.avatarID\" value=\"@PopForums.Resources.AvatarDelete\" /></p>\n\n\t\t<hr />\n\t\t<h3>@PopForums.Resources.Photo</h3>\n\t\t<input type=\"file\" :disabled=\"loading\" v-on:change=\"uploadImage($event.target)\" accept=\"image/*\" class=\"form-control mb-3\">\n\t\t<div><img v-if=\"user.imageID\" v-bind:src=\"'/Forums/Image/UserImage/' + user.imageID\" /></div>\n\t\t<p><input type=\"button\" class=\"btn btn-primary mt-3\" v-on:click=\"removeImage\" v-show=\"user.imageID\" value=\"@PopForums.Resources.PhotoDelete\" /></p>\n\n\t\t@if (!isOAuthOnly)\n\t\t{\n\t\t\t<hr />\n\t\t\t\t<div class=\"text-end\">\n\t\t\t\t<input type=\"button\" class=\"btn btn-primary mb-3\" value=\"@PopForums.Resources.DeleteUserButton\" v-on:click=\"deleteUser\" /><br />\n\t\t\t\t<input type=\"button\" class=\"btn btn-primary\" value=\"@PopForums.Resources.DeleteAndBanButton\" v-on:click=\"deleteAndBanUser\" />\n\t\t\t</div>\n\t\t}\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"UserRoles\">\n\t<main>\n\t\t<h2>@PopForums.Resources.UserRoles</h2>\n\t\t\n\t\t<div class=\"row\">\n\t\t\t<div class=\"mb-3 col-sm-8\"><input type=\"text\" v-model=\"newRole\" class=\"form-control\" /></div>\n\t\t\t<div class=\"mb-3 col-sm-3\"><input type=\"button\" value=\"@PopForums.Resources.CreateNewRole\" v-on:click=\"createRole\" class=\"btn btn-primary\" :disabled=\"loading\" /></div>\n\t\t</div>\n\t\t\n\t\t<div class=\"mb-3\">\n\t\t\t<select size=\"6\" class=\"form-select\" v-model=\"selectedAll\">\n\t\t\t\t<option v-for=\"r in roles\">{{r}}</option>\n\t\t\t</select>\n\t\t</div>\n\t\t<p><input type=\"submit\" value=\"@PopForums.Resources.DeleteSelectedRole\" v-on:click=\"deleteRole\" :disabled=\"loading\" class=\"btn btn-primary\" /></p>\n\n\t\t<p>@PopForums.Resources.PermRoles</p>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"UserImageApproval\">\n\t<main>\n\t\t<h2>@PopForums.Resources.UserImageApproval</h2>\n\t\t\n\t\t<div v-if=\"isNewUserImageApproved\"><p class=\"alert alert-warning\">@PopForums.Resources.UserImageApprovalNotReq</p></div>\n\t\t\n\t\t<div v-for=\"i in unapproved\">\n\t\t\t<p>\n\t\t\t\t{{i.user.name}} - <a v-bind:href=\"'mailto:' + i.user.email\" class=\"me-3\">{{i.user.email}}</a>\n\t\t\t\t<img v-bind:src=\"'/Forums/Image/UserImage/' + i.userImage.userImageID\" />\n\t\t\t</p>\n\t\t\t<p>\n\t\t\t\t<input type=\"button\" value=\"Approve\" class=\"btn btn-primary me-3\" v-on:click=\"approveImage(i.userImage.userImageID)\" />\n\t\t\t\t<input type=\"button\" value=\"@PopForums.Resources.Delete\" class=\"btn btn-primary\" v-on:click=\"deleteImage(i.userImage.userImageID)\" />\n\t\t\t</p>\n\t\t\t<hr />\n\t\t</div>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"EmailIpBan\">\n\t<main>\n\t\t<h2>@PopForums.Resources.EmailIpBan</h2>\n\n\t\t<h3>@PopForums.Resources.EmailBan</h3>\n\n\t\t<div class=\"row\">\n\t\t\t<div class=\"mb-3 col-sm-8\"><input type=\"text\" v-model=\"newEmail\" class=\"form-control\" /></div>\n\t\t\t<div class=\"mb-3 col-sm-3\"><input type=\"button\" value=\"@PopForums.Resources.Ban\" v-on:click=\"banEmail\" class=\"btn btn-primary\" :disabled=\"loading\" /></div>\n\t\t</div>\n\n\t\t<div class=\"mb-3\">\n\t\t\t<select size=\"6\" class=\"form-select\" v-model=\"selectedEmail\">\n\t\t\t\t<option v-for=\"e in emails\">{{e}}</option>\n\t\t\t</select>\n\t\t</div>\n\t\t<p><input type=\"submit\" value=\"@PopForums.Resources.BanRemove\" v-on:click=\"removeEmail\" :disabled=\"loading\" class=\"btn btn-primary\" /></p>\n\n\t\t<h3>@PopForums.Resources.IpBan</h3>\n\n\t\t<div class=\"row\">\n\t\t\t<div class=\"mb-3 col-sm-8\"><input type=\"text\" v-model=\"newIP\" class=\"form-control\" /></div>\n\t\t\t<div class=\"mb-3 col-sm-3\"><input type=\"button\" value=\"@PopForums.Resources.Ban\" v-on:click=\"banIP\" class=\"btn btn-primary\" :disabled=\"loading\" /></div>\n\t\t</div>\n\n\t\t<div class=\"mb-3\">\n\t\t\t<select size=\"6\" class=\"form-select\" v-model=\"selectedIP\">\n\t\t\t\t<option v-for=\"i in ips\">{{i}}</option>\n\t\t\t</select>\n\t\t</div>\n\t\t<p><input type=\"submit\" value=\"@PopForums.Resources.BanRemove\" v-on:click=\"removeIP\" :disabled=\"loading\" class=\"btn btn-primary\" /></p>\n\n\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"EmailUsers\">\n\t<main>\n\t\t<h2>@PopForums.Resources.EmailUsers</h2>\n\n\t\t<div class=\"mb-3\">\n\t\t\t<label>@PopForums.Resources.Subject</label>\n\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"subject\" />\n\t\t</div>\n\t\t<div class=\"mb-3\">\n\t\t\t<label>@PopForums.Resources.Body</label>\n\t\t\t<textarea class=\"form-control\" rows=\"10\" v-model=\"body\"></textarea>\n\t\t</div>\n\t\t<div class=\"mb-3\">\n\t\t\t<label>@PopForums.Resources.BodyHtml @PopForums.Resources.Optional</label>\n\t\t\t<textarea class=\"form-control\" rows=\"10\" v-model=\"htmlBody\"></textarea>\n\t\t</div>\n\t\t<input type=\"button\" value=\"@PopForums.Resources.Send\" v-on:click=\"sendMail\" class=\"btn btn-primary mr-3\" />\n\t\t<span v-if=\"isErrorVisible\" class=\"alert alert-warning ms-3\">{{errorMessage}}</span>\n\t\t<transition name=\"fade\"><span v-if=\"isSuccess\" class=\"alert alert-success ms-3\">@PopForums.Resources.EmailUsersQueued</span></transition>\n</main>\n</script>\n\n<script type=\"x-templates\" id=\"ScoringGame\">\n\t<main>\n\t\t<h2>@PopForums.Resources.ScoringGame</h2>\n\n\t\t<div class=\"mb-3\">\n\t\t\t<label>Interval (@PopForums.Resources.Millseconds)</label>\n\t\t\t<input type=\"text\" v-model=\"settings.scoringGameCalculatorInterval\" class=\"form-control\" />\n\t\t</div>\n\t\t<input type=\"button\" v-on:click=\"save('@PopForums.Resources.SettingsSaved')\" :disabled=\"loading\" value=\"@PopForums.Resources.Save\" class=\"btn btn-primary\" />\n\t\t<transition name=\"fade\"><span v-show=\"alert\" class=\"alert alert-success ms-3\">{{message}}</span></transition>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"EventDefinitions\">\n\t<main>\n\t\t<h2>@PopForums.Resources.EventDefinitions</h2>\n\n\t\t<p><input type=\"button\" class=\"btn btn-primary\" v-on:click=\"openNewEvent\" value=\"Add Event\" /></p>\n\n\t\t<table class=\"table table-striped\">\n            <thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>ID</th>\n\t\t\t\t\t<th>@PopForums.Resources.Description</th>\n\t\t\t\t\t<th>Point Value</th>\n\t\t\t\t\t<th>Publish to Feed</th>\n\t\t\t\t\t<th>&nbsp;</th>\n\t\t\t\t</tr>\n            </thead>\n            <tbody>\n\t\t\t\t<tr v-for=\"e in allEvents\">\n\t\t\t\t\t<td>{{e.eventDefinitionID}}</td>\n\t\t\t\t\t<td>{{e.description}}</td>\n\t\t\t\t\t<td>{{e.pointValue}}</td>\n\t\t\t\t\t<td><input type=\"checkbox\" disabled=\"disabled\" v-model=\"e.isPublishedToFeed\" class=\"form-check-input\" /></td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<input type=\"button\" value=\"Delete\" class=\"btn btn-primary\" v-show=\"!staticIDs.includes(e.eventDefinitionID)\" v-on:click=\"deleteEvent(e)\" :disabled=\"loading\" />\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n            </tbody>\n\t\t</table>\n\t\t<div class=\"modal\" tabindex=\"-1\" role=\"dialog\" ref=\"modal\">\n\t\t\t<div class=\"modal-dialog\" role=\"document\">\n\t\t\t\t<div class=\"modal-content\">\n\t\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t\t<h5 class=\"modal-title\">Add Event</h5>\n\t\t\t\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"EventDefinitionID\" class=\"col-sm-5 col-form-label\">EventDefinitionID</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-7\"><input type=\"text\" v-model=\"newEvent.eventDefinitionID\" class=\"form-control\" id=\"EventDefinitionID\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"description\" class=\"col-sm-5 col-form-label\">@PopForums.Resources.Description</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-7\"><input type=\"text\" v-model=\"newEvent.description\" class=\"form-control\" id=\"description\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"pointValue\" class=\"col-sm-5 col-form-label\"> Point Value</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-7\"><input type=\"text\" v-model=\"newEvent.pointValue\" class=\"form-control\" id=\"pointValue\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<div class=\"col-sm-7 offset-sm-5\">\n\t\t\t\t\t\t\t\t<label for=\"ispub\"><input type=\"checkbox\" v-model=\"newEvent.isPublishedToFeed\" id=\"ispub\" class=\"form-check-input\" /> Publish to Feed</label>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-footer\">\n\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-primary\" v-on:click=\"createEvent\" :disabled=\"loading\" data-bs-dismiss=\"modal\">@PopForums.Resources.Save</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"AwardDefinitions\">\n\t<main>\n\t\t<h2>@PopForums.Resources.AwardDefinitions</h2>\n\n\t\t<p><input type=\"button\" class=\"btn btn-primary\" v-on:click=\"openNewAward\" value=\"Add Award\" /></p>\n\n\t\t<table class=\"table table-striped\">\n            <thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>ID</th>\n\t\t\t\t\t<th>@PopForums.Resources.Title</th>\n\t\t\t\t\t<th>@PopForums.Resources.Description</th>\n\t\t\t\t\t<th>Single-time Award</th>\n\t\t\t\t\t<th>&nbsp;</th>\n\t\t\t\t</tr>\n            </thead>\n            <tbody>\n\t\t\t\t<tr v-for=\"a in allAwards\">\n\t\t\t\t\t<td><router-link :to=\"{path:'/awarddefinitions/'+a.awardDefinitionID}\">{{a.awardDefinitionID}}</router-link></td>\n\t\t\t\t\t<td>{{a.title}}</td>\n\t\t\t\t\t<td>{{a.description}}</td>\n\t\t\t\t\t<td><input type=\"checkbox\" disabled=\"disabled\" v-model=\"a.isSingleTimeAward\" class=\"form-check-input\" /></td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<input type=\"button\" value=\"Delete\" class=\"btn btn-primary\" v-on:click=\"deleteAward(a)\" :disabled=\"loading\" />\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n            </tbody>\n\t\t</table>\n\t\t<div class=\"modal\" tabindex=\"-1\" role=\"dialog\" ref=\"modal\">\n\t\t\t<div class=\"modal-dialog\" role=\"document\">\n\t\t\t\t<div class=\"modal-content\">\n\t\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t\t<h5 class=\"modal-title\">Add Award</h5>\n\t\t\t\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"awardid\" class=\"col-sm-5 col-form-label\">AwardDefinitionID</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-7\"><input type=\"text\" v-model=\"newAward.awardDefinitionID\" class=\"form-control\" id=\"awardid\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"title\" class=\"col-sm-5 col-form-label\">@PopForums.Resources.Title</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-7\"><input type=\"text\" v-model=\"newAward.title\" class=\"form-control\" id=\"title\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"description\" class=\"col-sm-5 col-form-label\">@PopForums.Resources.Description</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-7\"><input type=\"text\" v-model=\"newAward.description\" class=\"form-control\" id=\"description\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<div class=\"col-sm-7 offset-sm-5\">\n\t\t\t\t\t\t\t\t<label for=\"issingle\"><input type=\"checkbox\" v-model=\"newAward.isSingleTimeAward\" id=\"issingle\" class=\"form-check-input\" /> Single-time Award</label>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-footer\">\n\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-primary\" v-on:click=\"createAward\" :disabled=\"loading\" data-bs-dismiss=\"modal\">@PopForums.Resources.Save</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"AwardDefinitionDetail\">\n\t<main>\n\t\t<h2>Award Detail</h2>\n\n\t\t<table class=\"table\">\n            <tbody>\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Award Definition ID</td>\n\t\t\t\t\t<td>{{award.awardDefinitionID}}</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Title</td>\n\t\t\t\t\t<td>{{award.title}}</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Description</td>\n\t\t\t\t\t<td>{{award.description}}</td>\n\t\t\t\t</tr>\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Single-time award</td>\n\t\t\t\t\t<td><input type=\"checkbox\" disabled=\"disabled\" v-model=\"award.isSingleTimeAward\" class=\"form-check-input\" /></td>\n\t\t\t\t</tr>\n            </tbody>\n\t\t</table>\n\n\t\t<h3>Conditions</h3>\n\t\t<p><input type=\"button\" class=\"btn btn-primary\" v-on:click=\"openNewCondition\" value=\"Add Condition\" /></p>\n\t\t<table class=\"table\">\n            <tbody>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>EventDefinitionID</th>\n\t\t\t\t\t<th>Event Count</th>\n\t\t\t\t\t<th>&nbsp;</th>\n\t\t\t\t</tr>\n\t\t\t\t<tr v-for=\"c in conditions\">\n\t\t\t\t\t<td>{{c.eventDefinitionID}}</td>\n\t\t\t\t\t<td>{{c.eventCount}}</td>\n\t\t\t\t\t<td><input type=\"button\" v-on:click=\"deleteCondition(c)\" value=\"@PopForums.Resources.Delete\" class=\"btn btn-primary\" :disabled=\"loading\" /></td>\n\t\t\t\t</tr>\n            </tbody>\n\t\t</table>\n\t\t<div class=\"modal\" tabindex=\"-1\" role=\"dialog\" ref=\"modal\">\n\t\t\t<div class=\"modal-dialog\" role=\"document\">\n\t\t\t\t<div class=\"modal-content\">\n\t\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t\t<h5 class=\"modal-title\">Add Award</h5>\n\t\t\t\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"evid\" class=\"col-sm-5 col-form-label\">EventDefinitionID</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-7\">\n\t\t\t\t\t\t\t\t<select v-model=\"newCondition.eventDefinitionID\" class=\"form-select\" id=\"evid\">\n\t\t\t\t\t\t\t\t\t<option v-for=\"e in allEvents\">{{e.eventDefinitionID}}</option>\n\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t\t\t\t<label for=\"newcount\" class=\"col-sm-5 col-form-label\">Event Count</label>\n\t\t\t\t\t\t\t<div class=\"col-sm-7\"><input type=\"text\" v-model=\"newCondition.eventCount\" class=\"form-control\" id=\"newcount\" /></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-footer\">\n\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-primary\" v-on:click=\"createCondition\" :disabled=\"loading\" data-bs-dismiss=\"modal\">@PopForums.Resources.Save</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"ManualEvent\">\n\t<main>\n\t\t<h2>@PopForums.Resources.ManualEvent</h2>\n\n\t\t<div class=\"mb-3\">\n\t\t\t<label><input type=\"button\" class=\"btn btn-primary mb-3\" value=\"@PopForums.Resources.To\" v-on:click=\"openSearch\" /></label>\n\t\t\t<input type=\"text\" disabled v-model=\"selectedUser.name\" class=\"form-control\" />\n\t\t</div>\n\t\t<div class=\"mb-3\">\n\t\t\t<label>Feed Message</label>\n\t\t\t<input type=\"text\" v-model=\"message\" class=\"form-control\" />\n\t\t</div>\n\t\t<hr />\n\t\t<div class=\"mb-3\">\n\t\t\t<label>Points</label>\n\t\t\t<input type=\"text\" v-model=\"points\" class=\"form-control\" />\n\t\t</div>\n\t\t<input type=\"button\" class=\"btn btn-primary\" value=\"Publish Points\" v-on:click=\"createManualEvent\" :disabled=\"loading\" />\n\t\t<hr />\n\t\t<div class=\"mb-3\">\n\t\t\t<label>Event Definition ID</label>\n\t\t\t<select class=\"form-select\" v-model=\"eventDefinitionID\">\n\t\t\t\t<option v-for=\"e in allEvents\">{{e.eventDefinitionID}}</option>\n\t\t\t</select>\n\t\t</div>\n\t\t<input type=\"button\" class=\"btn btn-primary\" value=\"Publish Existing Event\" v-on:click=\"createExistingManualEvent\" :disabled=\"loading\" />\n\n\t\t<div class=\"modal\" tabindex=\"-1\" role=\"dialog\" ref=\"modal\">\n\t\t\t<div class=\"modal-dialog\" role=\"document\">\n\t\t\t\t<div class=\"modal-content\">\n\t\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t\t<h5 class=\"modal-title\">@PopForums.Resources.To</h5>\n\t\t\t\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t\t<div><input type=\"text\" class=\"form-control\" v-model=\"searchName\" v-on:keyup=\"updateList\" placeholder=\"@PopForums.Resources.Search\" /></div>\n\t\t\t\t\t\t<div class=\"mt-3\">\n\t\t\t\t\t\t\t<select size=\"6\" v-model=\"selectedUser\" class=\"form-select\" v-on:change=\"chooseUser\">\n\t\t\t\t\t\t\t\t<option v-for=\"u in searchResults\" v-bind:value=\"u\" data-bs-dismiss=\"modal\">{{u.name}}</option>\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"IPHistory\">\n\t<main>\n\t\t<h2>@PopForums.Resources.IpHistory</h2>\n\n\t\t<div role=\"form\">\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label>@PopForums.Resources.IP</label>\n\t\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"query.iP\" />\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label>@PopForums.Resources.StartDate</label>\n\t\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"query.start\" />\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label>@PopForums.Resources.EndDate</label>\n\t\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"query.end\" />\n\t\t\t</div>\n\t\t\t<input type=\"button\" value=\"@PopForums.Resources.Search\" class=\"btn btn-primary\" v-on:click=\"getHistory\" :disabled=\"loading\" />\n\t\t</div>\n\n\t\t<table class=\"table table-hover mt-3\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>@PopForums.Resources.EventTime</th>\n\t\t\t\t\t<th>@PopForums.Resources.Type</th>\n\t\t\t\t\t<th>@PopForums.Resources.Description</th>\n\t\t\t\t\t<th>@PopForums.Resources.UserID</th>\n\t\t\t\t\t<th>@PopForums.Resources.Name</th>\n\t\t\t\t\t<th>@PopForums.Resources.ID</th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t\t<tr v-for=\"item in history\">\n\t\t\t\t\t<td class=\"text-nowrap\">{{item.eventTime}}</td>\n\t\t\t\t\t<td>{{item.type}}</td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<span v-if=\"item.type === 'Post'\">\n\t\t\t\t\t\t\t<a v-bind:href=\"'/Forums/Forum/PostLink/' + item.id\" target=\"_blank\">{{item.description}}</a>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span v-else-if=\"item.type === 'SecurityLogEntry'\">\n\t\t\t\t\t\t\t{{item.description}}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span v-else>none</span>\n\t\t\t\t\t</td>\n\t\t\t\t\t<td>{{item.userID}}</td>\n\t\t\t\t\t<td>{{item.name}}</td>\n\t\t\t\t\t<td>{{item.id}}</td>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"SecurityLog\">\n\t<main>\n\t\t<h2>@PopForums.Resources.SecurityLog</h2>\n\n\t\t<div role=\"form\">\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label>@PopForums.Resources.Search</label>\n\t\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"query.searchTerm\" />\n\t\t\t</div>\n\t\t\t<div class=\"form-check form-check-inline mb-3\">\n\t\t\t\t<input class=\"form-check-input\" type=\"radio\" name=\"inlineRadioOptions\" id=\"inlineRadio2\" value=\"UserID\" v-model=\"query.type\">\n\t\t\t\t<label class=\"form-check-label\" for=\"inlineRadio2\">UserID</label>\n\t\t\t</div>\n\t\t\t<div class=\"form-check form-check-inline mb-3\">\n\t\t\t\t<input class=\"form-check-input\" type=\"radio\" name=\"inlineRadioOptions\" id=\"inlineRadio1\" value=\"Name\" v-model=\"query.type\">\n\t\t\t\t<label class=\"form-check-label\" for=\"inlineRadio1\">Name</label>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label>@PopForums.Resources.StartDate</label>\n\t\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"query.start\" />\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label>@PopForums.Resources.EndDate</label>\n\t\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"query.end\" />\n\t\t\t</div>\n\t\t\t<input type=\"button\" value=\"@PopForums.Resources.Search\" class=\"btn btn-primary\" v-on:click=\"getHistory\" :disabled=\"loading\" />\n\t\t</div>\n\n\t\t<table class=\"table table-hover mt-3\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>@PopForums.Resources.EventTime</th>\n\t\t\t\t\t<th>@PopForums.Resources.Event</th>\n\t\t\t\t\t<th>@PopForums.Resources.UserID</th>\n\t\t\t\t\t<th>@PopForums.Resources.UserIDTarget</th>\n\t\t\t\t\t<th>@PopForums.Resources.IP</th>\n\t\t\t\t\t<th>@PopForums.Resources.Message</th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t\t<tr v-for=\"item in history\">\n\t\t\t\t\t<td class=\"text-nowrap\">{{item.activityDate}}</td>\n\t\t\t\t\t<td>{{item.securityLogTypeString}}</td>\n\t\t\t\t\t<td>{{item.userID}}</td>\n\t\t\t\t\t<td>{{item.targetUserID}}</td>\n\t\t\t\t\t<td>{{item.ip}}</td>\n\t\t\t\t\t<td>{{item.message}}</td>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"ModerationLog\">\n\t<main>\n\t\t<h2>@PopForums.Resources.ModerationLog</h2>\n\n\t\t<div role=\"form\">\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label>@PopForums.Resources.StartDate</label>\n\t\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"query.start\" />\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label>@PopForums.Resources.EndDate</label>\n\t\t\t\t<input type=\"text\" class=\"form-control\" v-model=\"query.end\" />\n\t\t\t</div>\n\t\t\t<input type=\"button\" value=\"@PopForums.Resources.Search\" class=\"btn btn-primary\" v-on:click=\"getHistory\" :disabled=\"loading\" />\n\t\t</div>\n\n\t\t<table class=\"table table-hover mt-3\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>@PopForums.Resources.EventTime</th>\n\t\t\t\t\t<th>@PopForums.Resources.Name</th>\n\t\t\t\t\t<th>@PopForums.Resources.Type</th>\n\t\t\t\t\t<th>@PopForums.Resources.TopicID</th>\n\t\t\t\t\t<th>@PopForums.Resources.PostID</th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t\t<template v-for=\"item in history\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"text-nowrap\">{{item.timeStamp}}</td>\n\t\t\t\t\t\t<td>{{item.userName}}</td>\n\t\t\t\t\t\t<td>{{item.moderationTypeString}}</td>\n\t\t\t\t\t\t<td>{{item.topicID}}</td>\n\t\t\t\t\t\t<td><span v-if=\"item.postID\"><a v-bind:href=\"'/Forums/Forum/PostLink/' + item.postID\" target=\"_blank\">{{item.postID}}</a></span></td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr v-show=\"item.comment || item.oldText\">\n\t\t\t\t\t\t<td colspan=\"5\">\n\t\t\t\t\t\t\t<p v-show=\"item.comment\">@PopForums.Resources.Comment: {{item.comment}}</p>\n\t\t\t\t\t\t\t<div v-show=\"item.oldText\" class=\"alert alert-primary ms-3\" v-html=\"item.oldText\"></div>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</template>\n\t\t\t</tbody>\n\t\t</table>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"ErrorLog\">\n\t<main>\n\t\t<h2>@PopForums.Resources.ErrorLog</h2>\n\n\t\t<nav v-show=\"errorList.pageCount > 1\">\n\t\t\t<ul class=\"pagination\">\n\t\t\t\t<li class=\"page-item\" v-show=\"errorList.pageIndex > 1\" v-on:click=\"getMore(1)\"><a class=\"page-link\" href=\"javascript:void(0)\">|&laquo;</a></li>\n\t\t\t\t<li class=\"page-item\" v-show=\"errorList.pageIndex > 2\" v-on:click=\"getMore(errorList.pageIndex - 1)\"><a class=\"page-link\" href=\"javascript:void(0)\">&laquo;</a></li>\n\t\t\t\t<li class=\"page-item active\"><a class=\"page-link\">{{errorList.pageIndex}} of {{errorList.pageCount}}</a></li>\n\t\t\t\t<li class=\"page-item\" v-show=\"errorList.pageIndex < (errorList.pageCount - 1)\" v-on:click=\"getMore(errorList.pageIndex + 1)\"><a class=\"page-link\" href=\"javascript:void(0)\">&raquo;</a></li>\n\t\t\t\t<li class=\"page-item\" v-show=\"errorList.pageIndex < errorList.pageCount\" v-on:click=\"getMore(errorList.pageCount)\"><a class=\"page-link\" href=\"javascript:void(0)\">&raquo;|</a></li>\n\t\t\t</ul>\n\t\t</nav>\n\t\t\n\t\t<p><input type=\"button\" class=\"btn btn-primary\" value=\"@PopForums.Resources.DeleteAllErrors\" v-on:click=\"deleteAll\" /></p>\n\n\t\t<div v-for=\"item in errorList.list\">\n\t\t\t<p><b>{{item.severityString}}: {{item.timeStamp}} - {{item.message}}</b></p>\n\t\t\t<div class=\"small\" style=\"white-space: pre-wrap;\">{{item.stackTrace}}</div>\n\t\t\t<div class=\"small mt-3\" style=\"white-space: pre-wrap;\">{{item.data}}</div>\n\t\t\t<hr />\n\t\t</div>\n\t</main>\n</script>\n\n<script type=\"x-templates\" id=\"Services\">\n\t<main>\n\t\t<h2>@PopForums.Resources.Services</h2>\n\n\t\t<p><input type=\"button\" value=\"@PopForums.Resources.RemoveAll\" class=\"btn btn-primary\" v-on:click=\"clearAll\" /></p>\n\n\t\t<table class=\"table\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th>@PopForums.Resources.Name</th>\n\t\t\t\t\t<th>Machine</th>\n\t\t\t\t\t<th>@PopForums.Resources.Last</th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t\t<tr v-for=\"item in list\">\n\t\t\t\t\t<td>{{item.serviceName}}</td>\n\t\t\t\t\t<td>{{item.machineName}}</td>\n\t\t\t\t\t<td class=\"text-nowrap\">{{item.lastRun}}</td>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\t</main>\n</script>\n\n<environment include=\"Development\">\n\t<script src=\"~/PopForums/Admin.js\" asp-append-version=\"true\"></script>\n</environment>\n<environment exclude=\"Development\">\n\t<script src=\"~/PopForums/lib/PopForums/dist/Admin.min.js\" asp-append-version=\"true\"></script>\n</environment>\n</body>\n</html>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Favorites/Topics.cshtml",
    "content": "﻿@model PagedTopicContainer\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tViewBag.Title = PopForums.Resources.FavoriteTopics;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n<h1>@PopForums.Resources.FavoriteTopics</h1>\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\" asp-route-pageNumber=\"\">@PopForums.Resources.Recent</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item active\">@PopForums.Resources.FavoriteTopics</li>\n</ul>\n\n@if (Model == null)\n{\n\t<p>@PopForums.Resources.FavoriteMustBeLoggedIn</p>\n}\nelse\n{\n\tif (Model.Topics.Count == 0)\n\t{\n\t\t<p>@PopForums.Resources.FavoritesDontHave</p>\n\t}\n\n\t<pf-pagerLinks controllerName=\"Favorites\" actionName=\"Topics\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n\t<table id=\"TopicList\" class=\"table mt-3 table-hover\">\n\t\t@foreach (var topic in Model.Topics)\n\t\t{\n\t\t\t<tr @if (topic.IsDeleted) { <text> class=\"bg-warning\" </text>    }>\n\t\t\t\t<td class=\"newIndicator\"><a asp-controller=\"@ForumController.Name\" asp-action=\"GoToNewestPost\" asp-route-id=\"@topic.TopicID\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\" /></a></td>\n\t\t\t\t<td class=\"text-center\">\n\t\t\t\t\t<form asp-action=\"RemoveFavorite\" asp-controller=\"Favorites\" asp-antiforgery=\"false\">\n\t\t\t\t\t\t<input name=\"id\" id=\"id\" type=\"hidden\" value=\"@topic.TopicID\" />\n\t\t\t\t\t\t<button type=\"submit\" class=\"btn btn-primary\"><span class=\"icon icon-star-fill text-warning\"></span> @PopForums.Resources.FavoriteRemove</button>\n\t\t\t\t\t</form>\n\t\t\t\t</td>\n\t\t\t\t<td>\n\t\t\t\t\t<h2><a asp-controller=\"@ForumController.Name\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\" asp-route-pageNumber=\"\">@topic.Title</a></h2>\n\t\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t\t\t\t@PopForums.Resources.StartedBy: @topic.StartedByName @PopForums.Resources.In @Model.ForumTitles[topic.ForumID] | @PopForums.Resources.Views: @topic.ViewCount | @PopForums.Resources.Replies: @topic.ReplyCount | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@topic.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By @topic.LastPostName\n\t\t\t\t\t</small>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t}\n\t</table>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/Edit.cshtml",
    "content": "﻿@model PostEdit\n\n@{\n    ViewBag.Title = PopForums.Resources.EditPost;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar message = ViewBag.Message;\n}\n\n@section HeaderContent\n{\n\t<script type=\"text/javascript\" src=\"~/PopForums/lib/tinymce/tinymce.min.js\" asp-append-version=\"true\"></script>\n\t<script>\n\t\tPopForums.Ready(() => {\n\t\t\tlet form = document.getElementById(\"EditForm\");\n\t\t\tform.addEventListener(\"submit\", () => {\n\t            let imageIDinput = document.getElementById(\"PostImageIDs\");\n\t            imageIDinput.value = PopForums.userState.postImageIds;\n\t            return true;\n\t        });\n        });\n\t</script>\n}\n\n<h1>@PopForums.Resources.EditPost</h1>\n\n<form method=\"post\" id=\"EditForm\">\n\t<div class=\"postForm\" role=\"form\" class=\"mb-3\">\n\t\t@if (Model.IsFirstInTopic)\n\t\t{\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label for=\"Comment\" class=\"form-label\">@PopForums.Resources.Title</label>\n\t\t\t\t<input asp-for=\"Title\" type=\"text\" class=\"form-control\" />\n\t\t\t</div>\n\t\t}\n\t\t<pf-fulltext formid=\"FullText\" overridelistener=\"true\" value=\"@Model.FullText\"></pf-fulltext>\n\t\t<div class=\"form-check my-3\">\n\t\t\t<label for=\"IncludeSignature\" class=\"form-check-label\">@PopForums.Resources.IncludeSignature</label>\n\t\t\t<input type=\"checkbox\" asp-for=\"ShowSig\" class=\"form-check-input\" />\n\t\t</div>\n\t\t<div class=\"mb-3\">\n\t\t\t<label for=\"Comment\" class=\"form-label\">@PopForums.Resources.CommentsOptional</label>\n\t\t\t<textarea asp-for=\"Comment\" class=\"form-control\" rows=\"3\"></textarea>\n\t\t</div>\n\t\t<input type=\"hidden\" asp-for=\"IsPlainText\"/>\n        <input type=\"hidden\" asp-for=\"PostImageIDs\"/>\n\t</div>\n\t<input type=\"submit\" value=\"@PopForums.Resources.Save\" class=\"btn btn-primary\" />\n\t<pf-previewbutton textSourceSelector=\".postForm #FullText\" isplaintextselector=\".postForm #IsPlainText\" labelText=\"@PopForums.Resources.Preview\"></pf-previewbutton>\n\t@if (message != null) {<span class=\"alert alert-danger mt-3\">@message</span>}\n</form>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/Index.cshtml",
    "content": "﻿@inject IUserRetrievalShim UserRetrievalShim\n@model ForumTopicContainer\n@{\n\tViewBag.Title = Model.Forum.Title;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n\tvar forumStateSerialized = JsonSerializer.Serialize(Model.ForumState, new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n}\n\n@section HeaderContent{\n@if (user != null) {\n        <script src=\"~/PopForums/lib/tinymce/tinymce.min.js\" type=\"text/javascript\" asp-append-version=\"true\"></script>\n}\n<script type=\"text/javascript\">\n\tPopForums.currentForumState = Object.assign(new PopForums.ForumState(), @Html.Raw(forumStateSerialized));\n\tPopForums.currentForumState.setupForum();\n</script>\n}\n\n<h1>@Model.Forum.Title@if (Model.PermissionContext.UserCanPost){ <pf-topicbutton buttonclass=\"btn btn-primary\" buttontext=\"@PopForums.Resources.PostNewTopic\" forumid=\"@Model.Forum.ForumID\"></pf-topicbutton>}</h1>\n\n@if (Model.PermissionContext.UserCanPost)\n{\n\t<div id=\"NewTopic\" style=\"display: none;\"></div>\n}\nelse\n{\n\t<p class=\"alert alert-warning\">@Model.PermissionContext.DenialReason</p>\n}\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\" asp-route-pageNumber=\"\">@PopForums.Resources.Recent</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item active\">@Model.Forum.Title</li>\n</ul>\n\n<pf-pagerLinks controllerName=\"Forum\" actionName=\"Index\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n@if (user != null)\n{\n\t<form asp-action=\"MarkForumRead\" asp-controller=\"Forum\" asp-antiforgery=\"false\" asp-route-id=\"@Model.Forum.ForumID\">\n\t\t<input type=\"submit\" value=\"@PopForums.Resources.MarkForumRead\" class=\"btn btn-primary\" />\n\t</form>\n}\n\n<table id=\"TopicList\" class=\"table mt-3 table-hover\">\n    <tbody>\n\t@foreach (var topic in Model.Topics)\n\t{\n\t\t<tr @if (topic.IsDeleted){<text> class=\"bg-warning\" </text>}data-topicid=\"@topic.TopicID\">\n\t\t\t<td class=\"newIndicator\">\n\t\t\t\t@if (user == null)\n\t\t\t\t{\n\t\t\t\t\t<a asp-controller=\"Forum\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\"/></a>\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t<a asp-controller=\"@ForumController.Name\" asp-action=\"GoToNewestPost\" asp-route-id=\"@topic.TopicID\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\"/></a>\n\t\t\t\t}\n\t\t\t</td>\n\t\t\t<td class=\"w-100\">\n\t\t\t\t<h2><a asp-controller=\"@ForumController.Name\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\" asp-route-pageNumber=\"\">@topic.Title</a></h2>\n\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t\t\t@PopForums.Resources.StartedBy: @topic.StartedByName | @PopForums.Resources.Views: @topic.ViewCount.ToString(\"N0\") | @PopForums.Resources.Replies: @topic.ReplyCount.ToString(\"N0\") | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@topic.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By @topic.LastPostName\n\t\t\t\t</small>\n\t\t\t</td>\n\t\t</tr>\n\t}\n    <tr id=\"TopicTemplate\" data-topicid=\"\" class=\"hidden\">\n\t    <td class=\"newIndicator\">\n\t\t    <a href=\"\" class=\"indicatorLink\"><span class=\"topicIndicator icon icon-file-earmark-text-fill text-warning\"></span></a>\n\t    </td>\n\t    <td class=\"w-100\">\n\t\t    <h2><a class=\"titleLink\" href=\"\"></a></h2>\n\t\t    <small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t    @PopForums.Resources.StartedBy: <span class=\"startedByName\"></span> | @PopForums.Resources.Views: <span class=\"viewCount\"></span> | @PopForums.Resources.Replies: <span class=\"replyCount\"></span> | @PopForums.Resources.Last: <pf-formattedtime utctime=\"\"></pf-formattedtime> @PopForums.Resources.By <span class=\"lastPostName\"></span>\n\t\t    </small>\n\t    </td>\n    </tr>\n    </tbody>\n</table>\n\n<pf-pagerLinks controllerName=\"Forum\" actionName=\"Index\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\"/>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/IndexQA.cshtml",
    "content": "﻿@inject IUserRetrievalShim UserRetrievalShim\n@model ForumTopicContainer\n@{\n\tViewBag.Title = Model.Forum.Title;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n\tvar forumStateSerialized = JsonSerializer.Serialize(Model.ForumState, new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n}\n\n@section HeaderContent{\n\t@if (user != null)\n\t{\n        <script src=\"~/PopForums/lib/tinymce/tinymce.min.js\" type=\"text/javascript\" asp-append-version=\"true\"></script>\n\t}\n\n\t<script type=\"text/javascript\">\n\t\tPopForums.currentForumState = Object.assign(new PopForums.ForumState(), @Html.Raw(forumStateSerialized));\n\t</script>\n}\n\n<h1>@Model.Forum.Title@if (Model.PermissionContext.UserCanPost){ <pf-topicbutton buttonclass=\"btn btn-primary\" buttontext=\"@PopForums.Resources.AskAQuestion\" forumid=\"@Model.Forum.ForumID\"></pf-topicbutton>}</h1>\n\n@if (Model.PermissionContext.UserCanPost)\n{\n\t<div id=\"NewTopic\" style=\"display: none;\"></div>\n}\nelse\n{\n\t<p class=\"alert alert-warning\">@Model.PermissionContext.DenialReason</p>\n}\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\">@PopForums.Resources.Recent</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item active\">@Model.Forum.Title</li>\n</ul>\n\n<pf-pagerLinks controllerName=\"Forum\" actionName=\"Index\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n@if (user != null)\n{\n\t<form asp-action=\"MarkForumRead\" asp-controller=\"Forum\" asp-antiforgery=\"false\" asp-route-id=\"@Model.Forum.ForumID\">\n\t\t<input type=\"submit\" value=\"@PopForums.Resources.MarkForumRead\" class=\"btn btn-primary\" />\n\t</form>\n}\n\n<table id=\"TopicList\" class=\"table mt-3 table-hover\">\n\t@foreach (var topic in Model.Topics)\n\t{\n\t\t<tr @if (topic.IsDeleted) { <text> class=\"bg-warning\" </text> } data-topicid=\"@topic.TopicID\">\n\t\t\t<td class=\"newIndicator\">\n\t\t\t\t<a asp-controller=\"Forum\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\" /></a>\n\t\t\t</td>\n\t\t\t<td class=\"QAstate pt-3\">\n\t\t\t\t@if (topic.AnswerPostID.HasValue)\n\t\t\t\t{\n\t\t\t\t\t<span class=\"icon icon-check-circle-fill topicIndicator text-success\"></span>\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t<span class=\"icon icon-question-circle topicIndicator text-danger\"></span>\n\t\t\t\t}\n\t\t\t</td>\n\t\t\t<td>\n\t\t\t\t<h2><a asp-controller=\"@ForumController.Name\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\">@topic.Title</a></h2>\n\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t\t\t@PopForums.Resources.StartedBy: @topic.StartedByName | @PopForums.Resources.Views: @topic.ViewCount.ToString(\"N0\") | @PopForums.Resources.Replies: @topic.ReplyCount.ToString(\"N0\") | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@topic.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By @topic.LastPostName\n\t\t\t\t</small>\n\t\t\t</td>\n\t\t</tr>\n\t}\n</table>\n\n<pf-pagerLinks controllerName=\"Forum\" actionName=\"Index\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/ModeratorPanel.cshtml",
    "content": "﻿@model Topic\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n@if (user != null && user.IsInRole(PermanentRoles.Moderator))\n{\n\t<div id=\"ModeratorPanel\" class=\"bg-light mt-3 p-3\">\n\t\t<h3>@PopForums.Resources.Moderator</h3>\n\t\t<div class=\"form-check\">\n\t\t\t<label for=\"CloseOnReply\" class=\"form-check-label\">@PopForums.Resources.CloseOnReply</label>\n\t\t\t@Html.CheckBox(\"CloseOnReply\", new { @class = \"form-check-input\" })\n\t\t</div>\n\t\t<pf-topicmoderationlogbutton topicid=\"@Model.TopicID\" buttontext=\"@PopForums.Resources.ModerationLog\" buttonclass=\"btn btn-warning\"></pf-topicmoderationlogbutton>\n\t\t<ul class=\"list-inline\">\n\t\t\t<li class=\"list-inline-item\">\n\t\t\t\t<form asp-action=\"ToggleDeleted\" asp-controller=\"Moderator\" asp-antiforgery=\"false\" asp-route-id=\"@Model.TopicID\" class=\"inlineModForm\">\n\t\t\t\t\t<button type=\"submit\" class=\"btn btn-warning mt-2\">@if (Model.IsDeleted)\n                       {\n\t                       <span class=\"icon icon-recycle\"></span> @PopForums.Resources.Undelete\n                       }\n                       else\n                       {\n\t                       <span class=\"icon icon-trash3-fill\"></span> @PopForums.Resources.Delete\n                       }\n\t\t\t\t\t</button>\n\t\t\t\t</form>\n\t\t\t</li>\n\t\t\t<li class=\"list-inline-item\">\n\t\t\t\t<form asp-action=\"TogglePin\" asp-controller=\"Moderator\" asp-antiforgery=\"false\" asp-route-id=\"@Model.TopicID\" class=\"inlineModForm\">\n\t\t\t\t\t<button type=\"submit\" class=\"btn btn-warning mt-2\">@if (Model.IsPinned)\n                       {\n\t                       <span class=\"icon icon-pin-angle-fill\"></span> @PopForums.Resources.Unpin\n                       }\n                       else\n                       {\n\t                       <span class=\"icon icon-pin-angle\"></span> @PopForums.Resources.Pin\n                       }\n\t\t\t\t\t</button>\n\t\t\t\t</form>\n\t\t\t</li>\n\t\t\t<li class=\"list-inline-item\">\n\t\t\t\t<form asp-action=\"ToggleClosed\" asp-controller=\"Moderator\" asp-antiforgery=\"false\" asp-route-id=\"@Model.TopicID\" class=\"inlineModForm\">\n\t\t\t\t\t<button type=\"submit\" class=\"btn btn-warning mt-2\">@if (Model.IsClosed)\n                       {\n\t                       <span class=\"icon icon-lock-fill\"></span> @PopForums.Resources.Open\n                       }\n                       else\n                       {\n\t                       <span class=\"icon icon-lock\"></span> @PopForums.Resources.Close\n                       }\n\t\t\t\t\t</button>\n\t\t\t\t</form>\n\t\t\t</li>\n\t\t\t@if (user.IsInRole(PermanentRoles.Admin))\n\t\t\t{\n\t\t\t\t<li class=\"list-inline-item\">\n\t\t\t\t\t<form asp-action=\"DeleteTopicPermanently\" asp-controller=\"Moderator\" asp-antiforgery=\"false\" asp-route-id=\"@Model.TopicID\" class=\"inlineModForm\"><input type=\"submit\" value=\"@PopForums.Resources.DeletePermanently\" class=\"btn btn-warning mt-2\"/>\n\t\t\t\t\t</form></li>\n\t\t\t}\n\t\t</ul>\n\t\t<form asp-action=\"UpdateTopic\" asp-controller=\"Moderator\" asp-antiforgery=\"false\" asp-route-id=\"@Model.TopicID\" class=\"form-horizontal\" role=\"form\">\n\t\t\t@Html.Hidden(\"TopicID\", Model.TopicID)\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label for=\"NewTitle\" class=\"col-sm-2 form-label\">@PopForums.Resources.Title</label>\n\t\t\t\t<div class=\"col-sm-6\">\n\t\t\t\t\t@Html.TextBox(\"NewTitle\", Model.Title, new {@class = \"form-control\"})\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label for=\"NewForum\" class=\"col-sm-2 form-label\">@PopForums.Resources.Forum</label>\n\t\t\t\t<div class=\"col-sm-6\">\n\t\t\t\t\t<select id=\"NewForum\" name=\"NewForum\" class=\"form-select\" asp-items=\"ViewBag.CategorizedForums\"></select>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<div class=\"col-sm-6\">\n\t\t\t\t\t<input type=\"submit\" value=\"@PopForums.Resources.Update\" class=\"btn btn-warning\"/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</form>\n\t</div>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/NewComment.cshtml",
    "content": "﻿@model NewPost\n\n<template id=\"ReplyForm\">\n\n<h2>@PopForums.Resources.CreateNewReply</h2>\n\n\t<div id=\"NewReply\" class=\"postForm\" role=\"form\">\n\t\t<pf-fulltext formid=\"FullText\" overridelistener=\"true\"></pf-fulltext>\n\t\t@Html.HiddenFor(p => p.Title)\n\t\t@Html.HiddenFor(p => p.IncludeSignature)\n\t\t@Html.HiddenFor(p => p.ItemID)\n\t\t@Html.HiddenFor(p => p.IsPlainText)\n\t\t@Html.HiddenFor(p => p.IsImageEnabled)\n\t\t@Html.HiddenFor(p => p.ParentPostID)\n\t\t<div class=\"my-3\">\n\t\t\t<input type=\"button\" value=\"@PopForums.Resources.SubmitReply\" id=\"SubmitReply\" class=\"btn btn-primary\" />\n\t\t\t<pf-previewbutton textSourceSelector=\".postForm #FullText\" isplaintextselector=\".postForm #IsPlainText\" labelText=\"@PopForums.Resources.Preview\"></pf-previewbutton>\n\t\t\t<span id=\"PostResponseMessage\" class=\"alert alert-danger mt-3\"></span>\n\t\t</div>\n\t\n\t</div>\n\n</template>\n\n<pf-replyform templateID=\"ReplyForm\"></pf-replyform>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/NewReply.cshtml",
    "content": "﻿@model NewPost\n@{\n\tvar subhead = PopForums.Resources.CreateNewReply;\n\tvar replyButtonText = PopForums.Resources.SubmitReply;\n\tif (ViewBag.IsQA != null && (bool)ViewBag.IsQA)\n\t{\n\t\tsubhead = PopForums.Resources.PostAnswer;\n\t\treplyButtonText = PopForums.Resources.SubmitAnswer;\n\t}\t   \n}\n\n<template id=\"ReplyForm\">\n\t<h2>@subhead</h2>\n\n\t<p><pf-morepostsbeforereplybutton buttontext=\"@PopForums.Resources.ClickToLoadMorePosts\" buttonclass=\"btn btn-primary\"></pf-morepostsbeforereplybutton></p>\n\n\t<div class=\"postForm\" role=\"form\">\n\t\t@Html.HiddenFor(p => p.Title)\n\t\t<pf-fulltext formid=\"FullText\"></pf-fulltext>\n\t\t<div class=\"form-check my-3\">\n\t\t\t<label for=\"IncludeSignature\" class=\"form-check-label\">@PopForums.Resources.IncludeSignature</label>\n\t\t\t@Html.CheckBoxFor(p => p.IncludeSignature, new { @class = \"form-check-input\" })\n\t\t</div>\n\t\t@Html.HiddenFor(p => p.ItemID)\n\t\t@Html.HiddenFor(p => p.IsPlainText)\n\t\t@Html.HiddenFor(p => p.IsImageEnabled)\n\t\t@Html.HiddenFor(p => p.ParentPostID)\n\t\t<input type=\"button\" value=\"@replyButtonText\" id=\"SubmitReply\" class=\"btn btn-primary\" />\n\t\t<pf-previewbutton textSourceSelector=\".postForm #FullText\" isplaintextselector=\".postForm #IsPlainText\" labelText=\"@PopForums.Resources.Preview\"></pf-previewbutton>\n\t\t<div id=\"PostResponseMessage\" class=\"alert alert-danger mt-3\"></div>\n\t</div>\n</template>\n\n<pf-replyform templateid=\"ReplyForm\"></pf-replyform>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/NewTopic.cshtml",
    "content": "﻿@model NewPost\n\n<template id=\"TopicForm\">\n\n\t<h2>@PopForums.Resources.CreateNewTopic</h2>\n\t<div class=\"postForm\" role=\"form\">\n\t\t<div class=\"mb-3\">\n\t\t\t@Html.TextBoxFor(p => p.Title, new {@class = \"form-control\", placeholder = PopForums.Resources.Title})\n\t\t</div>\n\t\t<pf-fulltext formid=\"FullText\" overridelistener=\"true\"></pf-fulltext>\n\t\t<div class=\"form-check my-3\">\n\t\t\t<label for=\"IncludeSignature\" class=\"form-check-label\">\n\t\t\t\t@Html.CheckBoxFor(p => p.IncludeSignature, new {@class = \"form-check-input\"})\n\t\t\t\t@PopForums.Resources.IncludeSignature\n\t\t\t</label>\n\t\t</div>\n\t\t@Html.HiddenFor(p => p.ItemID)\n\t\t@Html.HiddenFor(p => p.IsPlainText)\n\t\t@Html.HiddenFor(p => p.IsImageEnabled)\n\t\t<input type=\"button\" value=\"@PopForums.Resources.SubmitNewTopic\" id=\"SubmitNewTopic\" class=\"btn btn-primary\"/>\n\t\t<pf-previewbutton textSourceSelector=\".postForm #FullText\" isplaintextselector=\".postForm #IsPlainText\" labelText=\"@PopForums.Resources.Preview\"></pf-previewbutton>\n\t\t<p id=\"PostResponseMessage\" class=\"alert alert-danger my-3\"></p>\n\t\t<hr class=\"mb-3\"/>\n\t</div>\n\n</template>\n\n<pf-topicform templateid=\"TopicForm\"></pf-topicform>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/PostItem.cshtml",
    "content": "﻿@inject IUserRetrievalShim UserRetrievalShim\n@using PopForums\n@model PostItemContainer\n@{\n\tvar user = UserRetrievalShim.GetUser();\n\tstring deleteLink;\n\tif (Model.Post.IsDeleted)\n\t{ deleteLink = PopForums.Resources.Undelete; }\n\telse if (Model.Post.IsFirstInTopic)\n\t{ deleteLink = PopForums.Resources.DeleteTopic; }\n\telse\n\t{ deleteLink = PopForums.Resources.Delete; }\n\tvar hideVanity = false;\n\tif (Model.Profile != null)\n\t{ hideVanity = Model.Profile.HideVanity; }\n\n\tvar isLoggedIn = user != null;\n\tvar isAuthor = user?.UserID == Model.Post.UserID;\n\tvar isVoted = isLoggedIn && !isAuthor && Model.VotedPostIDs.Contains(Model.Post.PostID);\n\tvar isIgnored = Model.IgnoreUserIDs.Contains(Model.Post.UserID);\n}\n\n<div class=\"@(isIgnored ? \"bg-light p-2 my-3\" : \"\")\" data-postID=\"@Model.Post.PostID\">\n@if (isIgnored)\n{\n\t<a class=\"d-block btn btn-scondary\" data-bs-toggle=\"collapse\" href=\"#post@(Model.Post.PostID)\" aria-expanded=\"false\" aria-controls=\"post@(Model.Post.PostID)\">@Resources.Ignored</a>\n}\n\n<div id=\"post@(Model.Post.PostID)\" class=\"@(Model.Post.IsDeleted ? \"bg-warning \" : \"\")@(isIgnored ? \"collapse \" : \"\")postItem my-3\" data-postID=\"@Model.Post.PostID\">\n\t<div class=\"postUserData bg-primary rounded mb-3 px-3 py-1\">\n\t\t<pf-postminiprofile username=\"@Model.Post.Name\" userid=\"@Model.Post.UserID\" miniprofileboxclass=\"miniProfileBox bg-light text-dark rounded\" usernameclass=\"text-light\"></pf-postminiprofile>\n\t\t<small class=\"postTime text-end text-light text-secondary\">\n\t\t\t@if (Model.User != null && Model.User.IsInRole(PermanentRoles.Moderator))\n\t\t\t{\n\t\t\t\t@:IP: @Model.Post.IP -\n\t\t\t}\n\t\t\t<pf-formattedtime utctime=\"@Model.Post.PostTime.ToString(\"o\")\"></pf-formattedtime>\n\t\t</small>\n\t</div>\n\t@if (!hideVanity && Model.Avatars.ContainsKey(Model.Post.UserID))\n\t{\n\t\t<img src=\"@Url.Action(\"Avatar\", \"Image\", new {id = (Model.Avatars)[Model.Post.UserID]})\" alt=\"@String.Format(PopForums.Resources.NameAvatar, Model.Post.Name)\" class=\"avatar float-end ms-3 mb-3 d-none d-sm-block\" />\n\t}\n    \n\t<div id=\"text@(Model.Post.PostID)\" class=\"postBody\">\n\t\t@Html.Raw(Model.Post.FullText)\n\t</div>\n\t\n\t@if (Model.Post.IsEdited && Model.Post.LastEditTime.HasValue)\n\t{\n\t\t<small>@String.Format(PopForums.Resources.NameLastEdit, Model.Post.LastEditName), <pf-formattedtime utctime=\"@Model.Post.LastEditTime.Value.ToString(\"o\")\"></pf-formattedtime></small>\n\t}\n\n\t@if (Model.Post.ShowSig && !hideVanity && (Model.Signatures).ContainsKey(Model.Post.UserID))\n\t{\n\t\t<hr />\n\t\t<div class=\"text-secondary\">@Html.Raw((Model.Signatures)[Model.Post.UserID])</div>\n\t}\n\t<div class=\"mt-3 d-flex postToolContainer\">\n\t\t<div class=\"my-auto\">\n\t\t\t<pf-votecount votes=\"@Model.Post.Votes\" postid=\"@Model.Post.PostID\" votescontainerclass=\"voters\" badgeclass=\"voteCount badge bg-primary align-text-top me-3\" isloggedin=\"@isLoggedIn.ToString()\" isauthor=\"@isAuthor.ToString()\" isvoted=\"@isVoted.ToString()\" votebuttonclass=\"\" containerclass=\"voteUpContainer\"></pf-votecount>\n\t\t</div>\n\t\t<div class=\"toolButton\"><a asp-controller=\"Forum\" asp-action=\"PostLink\" asp-route-id=\"@Model.Post.PostID\" title=\"@PopForums.Resources.Link\" rel=\"nofollow\"><span class=\"icon icon-link\"></span></a></div>\n\t\t@if (user != null && Model.Topic != null && Model.Topic.IsClosed == false)\n\t\t{\n\t\t\t<div class=\"toolButton\"><pf-replybutton buttonclass=\"btn-link icon icon-reply-fill\" buttontext=\"@PopForums.Resources.Reply\" topicid=\"@Model.Topic.TopicID\" postid=\"@Model.Post.PostID\" overridedisplay=\"true\" isposttool=\"true\"></pf-replybutton></div>\n\t\t\t<div class=\"toolButton\"><pf-quotebutton name=\"@Model.Post.Name\" buttonclass=\"btn-link icon icon-quote\" containerid=\"text@(Model.Post.PostID)\" buttontext=\"@PopForums.Resources.Quote\" tip=\"@PopForums.Resources.SelectText\" postid=\"@Model.Post.PostID\" callback=\"PopForums.currentTopicState.nextQuote\"></pf-quotebutton></div>\n\t\t}\n\t\t@if (Model.User.IsPostEditable(Model.Post))\n\t\t{\n\t\t\t<div class=\"toolButton\"><a asp-controller=\"Forum\" asp-action=\"Edit\" asp-route-id=\"@Model.Post.PostID\" title=\"@PopForums.Resources.Edit\"><span class=\"icon icon-pencil-square\"></span></a></div>\n\t\t}\n\t\t<div class=\"ms-auto d-flex\">\n\t\t\t@if (user != null)\n\t\t\t{\n\t\t\t\t@if (isIgnored)\n\t\t\t\t{\n\t\t\t\t\t<div class=\"toolButton\">\n\t\t\t\t\t\t<form method=\"post\" asp-action=\"Remove\" asp-controller=\"Ignore\" asp-antiforgery=\"false\" asp-route-userID=\"@Model.Post.UserID\" asp-route-postID=\"@Model.Post.PostID\" class=\"deletePostForm d-inline-block\"><button type=\"submit\" title=\"@PopForums.Resources.Ignore\" class=\"btn-link\"><span class=\"icon icon-hand-thumbs-down-fill\"></span></button></form>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t<div class=\"toolButton\">\n\t\t\t\t\t\t<form method=\"post\" asp-action=\"Add\" asp-controller=\"Ignore\" asp-antiforgery=\"false\" asp-route-userid=\"@Model.Post.UserID\" asp-route-postID=\"@Model.Post.PostID\" class=\"deletePostForm d-inline-block\"><button type=\"submit\" title=\"@PopForums.Resources.Ignore\" class=\"btn-link\"><span class=\"icon icon-hand-thumbs-down\"></span></button></form>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t}\n\t\t\t@if (Model.User.IsPostEditable(Model.Post))\n\t\t\t{\n\t\t\t\t@if (Model.Post.IsDeleted)\n\t\t\t\t{\n\t\t\t\t\t<div class=\"toolButton\">\n\t\t\t\t\t\t<form asp-action=\"UndeletePost\" asp-controller=\"Moderator\" asp-antiforgery=\"false\" asp-route-id=\"@Model.Post.PostID\" class=\"deletePostForm d-inline-block\"><button type=\"submit\" title=\"@PopForums.Resources.Undelete\" class=\"btn-link\"><span class=\"icon icon-recycle\"></span></button></form>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t<div class=\"toolButton\">\n\t\t\t\t\t\t<form asp-action=\"DeletePost\" asp-controller=\"Forum\" asp-antiforgery=\"false\" asp-route-id=\"@Model.Post.PostID\" class=\"deletePostForm d-inline-block\"><button type=\"submit\" title=\"@deleteLink\" class=\"btn-link\"><span class=\"icon icon-trash3-fill\"></span></button></form>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t}\n\t\t</div>\n\t</div>\n\t@if (Model.Post.IsEdited && Model.User != null && Model.User.IsInRole(PermanentRoles.Moderator))\n\t{\n\t\t<div>\n\t\t\t<pf-postmoderationlogbutton buttonclass=\"btn-link\" buttontext=\"@PopForums.Resources.ModerationLog\" postid=\"@Model.Post.PostID\" parentselectortoappendto=\".postItem\"></pf-postmoderationlogbutton>\n\t\t</div>\n\t}\n</div>\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/QAPost.cshtml",
    "content": "﻿@model QAPostItemContainer\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tvar user = UserRetrievalShim.GetUser();\n\tvar profile = UserRetrievalShim.GetProfile();\n\tvar hideVanity = false;\n\tif (profile != null)\n\t{ hideVanity = profile.HideVanity; }\n\n\tstring deleteLink;\n\tif (Model.Post.IsDeleted)\n\t{ deleteLink = PopForums.Resources.Undelete; }\n\telse if (Model.Post.IsFirstInTopic)\n\t{ deleteLink = PopForums.Resources.DeleteTopic; }\n\telse\n\t{ deleteLink = PopForums.Resources.Delete; }\n    \n\tvar isLoggedIn = user != null;\n\tvar isAuthor = user?.UserID == Model.Post.UserID;\n\tvar isVoted = isLoggedIn && !isAuthor && Model.VotedPostIDs.Contains(Model.Post.PostID);\n}\n<a name=\"@Model.Post.PostID\"></a>\n<div class=\"@(Model.Post.IsDeleted ? \"bg-warning \" : \"\") row postItem flex-nowrap my-3\" data-postID=\"@Model.Post.PostID\" data-topicid=\"@Model.Topic.TopicID\">\n\t<div class=\"answerData col-2 text-center\">\n\t\t<pf-votecount votes=\"@Model.Post.Votes\" postid=\"@Model.Post.PostID\" votescontainerclass=\"voters\" badgeclass=\"voteCount badge bg-primary m-auto d-block mb-3 fs-5\" isloggedin=\"@isLoggedIn.ToString()\" isauthor=\"@isAuthor.ToString()\" isvoted=\"@isVoted.ToString()\" votebuttonclass=\"m-auto fs-2\" containerclass=\"voteUpContainerVert\"></pf-votecount>\n\n\t\t<pf-answerbutton postid=\"@Model.Post.PostID\" topicid=\"@Model.Topic.TopicID\" answerstatusclass=\"answerStatus\" chooseanswertext=\"@PopForums.Resources.ChooseAnswer\" answerpostid=\"@Model.Topic.AnswerPostID\" userid=\"@user?.UserID\" startedbyuserid=\"@Model.Topic.StartedByUserID\" isfirstintopic=\"@Model.Post.IsFirstInTopic.ToString()\"></pf-answerbutton>\n\t</div>\n\t<div class=\"col-10\">\n\t\t<div class=\"postUserData bg-primary rounded mb-2 px-3 py-1\">\n\t\t\t<pf-postminiprofile username=\"@Model.Post.Name\" userid=\"@Model.Post.UserID\" miniprofileboxclass=\"miniProfileBox bg-light text-dark rounded\" usernameclass=\"text-light\"></pf-postminiprofile>\n\t\t\t<small class=\"postTime text-end text-light\">\n\t\t\t\t@if (user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\t\t{\n\t\t\t\t\t@:IP: @Model.Post.IP -\n\t\t\t\t}\n\t\t\t\t<pf-formattedtime utctime=\"@Model.Post.PostTime.ToString(\"o\")\"></pf-formattedtime>\n\t\t\t</small>\n\t\t</div>\n\t\t@if (!hideVanity && Model.Avatars.ContainsKey(Model.Post.UserID))\n\t\t{\n\t\t\t<img src=\"@Url.Action(\"Avatar\", \"Image\", new { id = (Model.Avatars)[Model.Post.UserID] })\" alt=\"@String.Format(PopForums.Resources.NameAvatar, Model.Post.Name)\" class=\"avatar float-end ms-3 mb-3 d-none d-sm-block\" />\n\t\t}\n\n\t\t<div class=\"@if (Model.PostWithChildren.LastReadTime.HasValue && Model.PostWithChildren.Post.PostTime > Model.PostWithChildren.LastReadTime)\n\t{<text>newPostBlock</text>}\">@Html.Raw(Model.Post.FullText)</div>\n\t\t@if (Model.Post.IsEdited && Model.Post.LastEditTime.HasValue)\n\t\t{\n\t\t\t<small>@String.Format(PopForums.Resources.NameLastEdit, Model.Post.LastEditName), <pf-formattedtime utctime=\"@Model.Post.LastEditTime.Value.ToString(\"o\")\"></pf-formattedtime></small>\n\t\t}\n\n\t\t@if (Model.Post.ShowSig && !hideVanity && (Model.Signatures).ContainsKey(Model.Post.UserID))\n\t\t{\n\t\t\t<hr />\n\t\t\t<div class=\"text-secondary\">@Html.Raw((Model.Signatures)[Model.Post.UserID])</div>\n\t\t}\n\t\t<div class=\"superToolBox\">\n\t\t\t<div class=\"my-3 d-flex postToolContainer\">\n\t\t\t\t<div class=\"toolButton\"><a asp-controller=\"Forum\" asp-action=\"PostLink\" asp-route-id=\"@Model.Post.PostID\" title=\"@PopForums.Resources.Link\" rel=\"nofollow\"><span class=\"icon icon-link\"></span></a></div>\n\t\t\t\t@if (user != null && Model.Topic != null && Model.Topic.IsClosed == false)\n\t\t\t\t{\n\t\t\t\t\t<div class=\"toolButton\"><pf-commentbutton postid=\"@Model.Post.PostID\" topicid=\"@Model.Topic.TopicID\" buttontext=\"@PopForums.Resources.Reply\" buttonclass=\"btn-link icon icon-reply-fill\"></pf-commentbutton></div>\n\t\t\t\t}\n\t\t\t\t@if (user.IsPostEditable(Model.Post))\n\t\t\t\t{\n\t\t\t\t\t<div class=\"toolButton\"><a asp-controller=\"Forum\" asp-action=\"Edit\" asp-route-id=\"@Model.Post.PostID\" title=\"@PopForums.Resources.Edit\"><span class=\"icon icon-pencil-square\"></span></a></div>\n\t\t\t\t}\n\t\t\t\t@if (Model.User.IsPostEditable(Model.Post))\n\t\t\t\t{\n\t\t\t\t\t@if (Model.Post.IsDeleted)\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"toolButton ms-auto\">\n\t\t\t\t\t\t\t<form asp-action=\"UndeletePost\" asp-controller=\"Moderator\" asp-antiforgery=\"false\" asp-route-id=\"@Model.Post.PostID\" class=\"deletePostForm d-inline-block\"><button type=\"submit\" title=\"@PopForums.Resources.Undelete\" class=\"btn-link\"><span class=\"icon icon-recycle\"></span></button></form>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"toolButton ms-auto\">\n\t\t\t\t\t\t\t<form asp-action=\"DeletePost\" asp-controller=\"Forum\" asp-antiforgery=\"false\" asp-route-id=\"@Model.Post.PostID\" class=\"deletePostForm d-inline-block\"><button type=\"submit\" title=\"@deleteLink\" class=\"btn-link\"><span class=\"icon icon-trash3-fill\"></span></button></form>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t</div>\n\t\t\t@if (Model.Post.IsEdited && user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\t{\n\t\t\t\t<div class=\"my-3\">\n\t\t\t\t\t<pf-postmoderationlogbutton buttonclass=\"btn-link\" buttontext=\"@PopForums.Resources.ModerationLog\" postid=\"@Model.Post.PostID\" parentselectortoappendto=\".superToolBox\"></pf-postmoderationlogbutton>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\t\t@if (Model.PostWithChildren.Children != null && Model.PostWithChildren.Children.Count > 0)\n\t\t{\n\t\t\tforeach (var comment in Model.PostWithChildren.Children)\n\t\t\t{\n\t\t\t\t<a name=\"@comment.PostID\"></a>\n\t\t\t\t<div class=\"@if(comment.IsDeleted){<text>bg-warning</text>} card border-secondary mb-3 qaComment ms-3 p-3 @if (Model.PostWithChildren.LastReadTime.HasValue && comment.PostTime > Model.PostWithChildren.LastReadTime)\n\t{<text>newPostBlock</text>}\" data-postID=\"@comment.PostID\">\n\t\t\t\t\t<p class=\"text-muted\">\n\t\t\t\t\t\t@Html.ActionLink(comment.Name, \"ViewProfile\", AccountController.Name, new { id = comment.UserID }, null) - <small><pf-formattedtime utctime=\"@comment.PostTime.ToString(\"o\")\"></pf-formattedtime>\n\t\t\t\t\t\t@if (user != null && user.IsInRole(PermanentRoles.Moderator))\n\t\t\t\t\t\t{<text> - IP: </text>@comment.IP}</small>\n\t\t\t\t\t</p>\n\t\t\t\t\t@Html.Raw(comment.FullText)\n\t\t\t\t\t@if (user.IsPostEditable(comment))\n\t\t\t\t\t{\n\t\t\t\t\t\tif (comment.IsDeleted)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<form asp-controller=\"Moderator\" asp-action=\"UndeletePost\" asp-route-id=\"@comment.PostID\" method=\"post\" class=\"ms-auto fs-5\">\n\t\t\t\t\t\t\t\t<button type=\"submit\" title=\"@PopForums.Resources.Undelete\" class=\"btn-link icon icon-recycle text-decoration-none\"></button>\n\t\t\t\t\t\t\t</form>\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<form asp-controller=\"Forum\" asp-action=\"DeletePost\" asp-route-id=\"@comment.PostID\" method=\"post\" class=\"ms-auto fs-5\">\n\t\t\t\t\t\t\t\t<button type=\"submit\" title=\"@PopForums.Resources.Delete\" class=\"btn-link icon icon-trash3-fill text-decoration-none\"></button>\n\t\t\t\t\t\t\t</form>\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t}\n\t\t\tif (user != null && Model.PostWithChildren.Children.Count > 0 && !Model.Topic.IsClosed)\n\t\t\t{\n\t\t\t\t<p><pf-commentbutton postid=\"@Model.Post.PostID\" topicid=\"@Model.Topic.TopicID\" buttontext=\"@PopForums.Resources.Reply\" buttonclass=\"btn btn-primary btn-sm\"></pf-commentbutton></p>\n\t\t\t}\n\t\t}\n\t\t<div class=\"commentHolder\"></div>\n\t</div>\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/Recent.cshtml",
    "content": "﻿@model PagedTopicContainer\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tViewBag.Title = PopForums.Resources.RecentTopics;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n\tvar forumStateSerialized = JsonSerializer.Serialize((ForumState) ViewBag.ForumState, new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n}\n\n@section HeaderContent {\n\t<script type=\"text/javascript\" language=\"javascript\">\n\t\tPopForums.currentForumState = Object.assign(new PopForums.ForumState(), @Html.Raw(forumStateSerialized));\n\t\tPopForums.currentForumState.recentListen();\n\t</script>\n}\n\n<h1>@PopForums.Resources.Recent</h1>\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item active\">@PopForums.Resources.Recent</li>\n</ul>\n\n<pf-pagerLinks controllerName=\"Forum\" actionName=\"Recent\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n<table id=\"TopicList\" class=\"table mt-3 table-hover\">\n\t<tbody>\n\t@foreach (var topic in Model.Topics)\n\t{\n\t\t<tr @if (topic.IsDeleted) {<text> class=\"bg-warning\" </text>}data-topicid=\"@topic.TopicID\">\n\t\t\t<td class=\"newIndicator\">\n\t\t\t\t@if (user == null)\n\t\t\t\t{\n\t\t\t\t\t<a asp-controller=\"Forum\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\"/></a>\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t<a asp-controller=\"@ForumController.Name\" asp-action=\"GoToNewestPost\" asp-route-id=\"@topic.TopicID\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\"/></a>\n\t\t\t\t}\n\t\t\t</td>\n\t\t\t<td class=\"w-100\">\n\t\t\t\t<h2>@Html.ActionLink(topic.Title, \"Topic\", \"Forum\", new\n\t\t\t\t    {\n\t\t\t\t\t    id = topic.UrlName, pageNumber = 1\n\t\t\t\t    }, null)</h2>\n\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t\t\t@PopForums.Resources.StartedBy: @topic.StartedByName @PopForums.Resources.In @Model.ForumTitles[topic.ForumID] | @PopForums.Resources.Views: @topic.ViewCount | @PopForums.Resources.Replies: @topic.ReplyCount | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@topic.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By @topic.LastPostName\n\t\t\t\t</small>\n\t\t\t</td>\n\t\t</tr>\n\t}\n\t<tr id=\"TopicTemplate\" data-topicid=\"\" class=\"hidden\">\n\t\t<td class=\"newIndicator\">\n\t\t\t<a href=\"\" class=\"indicatorLink\"><span class=\"topicIndicator icon icon-file-earmark-text-fill text-warning\"></span></a>\n\t\t</td>\n\t\t<td class=\"w-100\">\n\t\t\t<h2><a class=\"titleLink\" href=\"\"></a></h2>\n\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t\t@PopForums.Resources.StartedBy: <span class=\"startedByName\"></span> @PopForums.Resources.In <span class=\"forumTitle\"></span> | @PopForums.Resources.Views: <span class=\"viewCount\"></span> | @PopForums.Resources.Replies: <span class=\"replyCount\"></span> | @PopForums.Resources.Last: <pf-formattedtime utctime=\"\"></pf-formattedtime> @PopForums.Resources.By <span class=\"lastPostName\"></span>\n\t\t\t</small>\n\t\t</td>\n\t</tr>\n    </tbody>\n</table>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/Topic.cshtml",
    "content": "﻿@model TopicContainer\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tViewBag.Title = Model.Topic.Title;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n\tvar profile = UserRetrievalShim.GetProfile();\n\tvar topicStateSerialized = JsonSerializer.Serialize(Model.TopicState, new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n}\n\n@section HeaderContent {\n    <script src=\"~/PopForums/lib/tinymce/tinymce.min.js\" type=\"text/javascript\" asp-append-version=\"true\"></script>\n\t<script type=\"text/javascript\">\n\t\tPopForums.currentTopicState = Object.assign(new PopForums.TopicState(), @Html.Raw(topicStateSerialized));\n\t\tPopForums.currentTopicState.setupTopic();\n\t</script>\n}\n\n<h1>\n\t@if (user != null)\n\t{\n\t\t<pf-subscribebutton buttonclass=\"btn-link text-decoration-none\" subscribetext=\"@PopForums.Resources.Subscribe\" unsubscribetext=\"@PopForums.Resources.Unsubscribe\"></pf-subscribebutton>\n\t\t<pf-favoritebutton buttonclass=\"btn-link text-decoration-none\" makefavoritetext=\"@PopForums.Resources.FavoriteMake\" removefavoritetext=\"@PopForums.Resources.FavoriteRemove\"></pf-favoritebutton>\n\t}\n\t@Model.Topic.Title\n</h1>\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\" asp-route-pageNumber=\"\">@PopForums.Resources.Recent</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"Forum\" asp-action=\"Index\" asp-route-urlName=\"@Model.Forum.UrlName\" asp-route-pageNumber=\"\">@Model.Forum.Title</a></li>\n\t<li class=\"breadcrumb-item active\">@Model.Topic.Title</li>\n</ul>\n\n@if (user != null) {\n\t<p id=\"AsyncResponse\"></p>\n}\n\n<pf-pagerLinks controllerName=\"Forum\" actionName=\"Topic\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks mt-3\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n@if (Model.PagerContext.PageIndex > 1)\n{\n\t<pf-previouspostsbutton buttontext=\"@PopForums.Resources.ShowPreviousPosts\" buttonclass=\"btn btn-primary\"></pf-previouspostsbutton>\n}\n\n<div id=\"PostStream\">\n\n\t@foreach(var post in Model.Posts) {\n\t\t@await Html.PartialAsync(\"~/Areas/Forums/Views/Forum/PostItem.cshtml\", new PostItemContainer { Post = post, VotedPostIDs = Model.VotedPostIDs, Signatures = Model.Signatures, Avatars = Model.Avatars, User = user, Profile = profile, Topic = Model.Topic, IgnoreUserIDs = Model.IgnoreUserIDs });\n\t}\n\n</div>\n\n<p><pf-morepostsbutton buttontext=\"@PopForums.Resources.ShowMorePosts\" buttonclass=\"btn btn-primary\"></pf-morepostsbutton></p>\n\n<pf-pagerLinks controllerName=\"Forum\" actionName=\"Topic\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n<div id=\"StreamBottom\"></div>\n\n@if (Model.PermissionContext.UserCanPost)\n{\n\t<div id=\"NewReply\"></div>\n\t<pf-replybutton buttonclass=\"btn btn-primary\" buttontext=\"@PopForums.Resources.PostReply\" topicid=\"@Model.Topic.TopicID\" postid=\"\" overridedisplay=\"false\"></pf-replybutton>\n}\nelse\n{\n\t<p class=\"alert alert-warning\">@Model.PermissionContext.DenialReason</p>\n}\n\n@await Html.PartialAsync(\"ModeratorPanel\", Model.Topic)"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/TopicPage.cshtml",
    "content": "﻿@model TopicContainer\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tvar user = UserRetrievalShim.GetUser();\n\tvar profile = UserRetrievalShim.GetProfile();\n\tvar routeParameters = new Dictionary<string, object> {{\"id\", Model.Topic.UrlName}};\n}\n<div>\n@foreach (var post in Model.Posts)\n{\n\t@await Html.PartialAsync(\"~/Areas/Forums/Views/Forum/PostItem.cshtml\", new PostItemContainer { Post = post, VotedPostIDs = Model.VotedPostIDs, Signatures = Model.Signatures, Avatars = Model.Avatars, User = user, Profile = profile, Topic = Model.Topic, IgnoreUserIDs = Model.IgnoreUserIDs});\n}\n<pf-pagerLinks controllerName=\"Forum\" actionName=\"Topic\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm mt-3 pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"page-item currentPager active\" low=\"@ViewBag.Low\" high=\"@ViewBag.High\" routeParameters=\"@routeParameters\" />\n@Html.Hidden(\"LastPostID\", Model.Posts.Last().PostID, new { @class = \"lastPostID\" })\n@Html.Hidden(\"PageCount\", Model.PagerContext.PageCount, new { @class = \"pageCount\" })\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/TopicQA.cshtml",
    "content": "﻿@model TopicContainerForQA\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tViewBag.Title = Model.Topic.Title;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n\tvar profile = UserRetrievalShim.GetProfile();\n\tvar topicStateSerialized = JsonSerializer.Serialize(Model.TopicState, new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n}\n\n@section HeaderContent {\n    <script src=\"~/PopForums/lib/tinymce/tinymce.min.js\" type=\"text/javascript\"></script>\n<script type=\"text/javascript\">\n\tPopForums.currentTopicState = Object.assign(new PopForums.TopicState(), @Html.Raw(topicStateSerialized));\n\tPopForums.currentTopicState.setupTopic();\n</script>\n}\n\n<h1>\n\t@if (user != null)\n\t{\n\t\t<pf-subscribebutton buttonclass=\"btn-link text-decoration-none\" subscribetext=\"@PopForums.Resources.Subscribe\" unsubscribetext=\"@PopForums.Resources.Unsubscribe\"></pf-subscribebutton>\n\t\t<pf-favoritebutton buttonclass=\"btn-link text-decoration-none\" makefavoritetext=\"@PopForums.Resources.FavoriteMake\" removefavoritetext=\"@PopForums.Resources.FavoriteRemove\"></pf-favoritebutton>\n\t}\n\t@Model.Topic.Title\n</h1>\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\">@PopForums.Resources.Recent</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Index\" asp-route-urlName=\"@Model.Forum.UrlName\" asp-route-pageNumber=\"1\">@Model.Forum.Title</a></li>\n\t<li class=\"breadcrumb-item active\">@Model.Topic.Title</li>\n</ul>\n\n@if (user != null) {\n\t<p id=\"AsyncResponse\"></p>\n}\n\n<div id=\"PostStream\">\n\n\t@{\n\t\t@(await Html.PartialAsync(\"QAPost\", new QAPostItemContainer { PostWithChildren = Model.QuestionPostWithComments, Topic = Model.Topic, Avatars = Model.Avatars, Signatures = Model.Signatures, User = user, Profile = profile, VotedPostIDs = Model.VotedPostIDs, Post = Model.QuestionPostWithComments.Post }))\n\t}\n\n\t@if (Model.PermissionContext.UserCanPost)\n\t{\n\t\t<div id=\"NewReply\" style=\"display: none;\"></div>\n\t\t<pf-replybutton buttonclass=\"btn btn-primary\" buttontext=\"@PopForums.Resources.PostAnswer\" topicid=\"@Model.Topic.TopicID\" postid=\"\"></pf-replybutton>\n\t}\n\telse\n\t{\n\t\t<p class=\"alert alert-warning\">@Model.PermissionContext.DenialReason</p>\n\t}\n\n\t<h2>@PopForums.Resources.Answers</h2>\n\n\t@foreach (var answer in Model.AnswersWithComments)\n\t{\n\t\t@await Html.PartialAsync(\"QAPost\", new QAPostItemContainer { PostWithChildren = answer, Topic = Model.Topic, Avatars = Model.Avatars, Signatures = Model.Signatures, User = user, Profile = profile, VotedPostIDs = Model.VotedPostIDs, Post = answer.Post });\n\t}\n\n</div>\n\n@await Html.PartialAsync(\"ModeratorPanel\", Model.Topic)"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Forum/Voters.cshtml",
    "content": "﻿@model VotePostContainer\n<ul>\n@foreach (var user in Model.Voters)\n{\n\t<li>@Html.ActionLink(user.Value, \"ViewProfile\", \"Account\", new { id = user.Key }, new { target = \"_blank\" })</li>\n}\n</ul>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Home/Index.cshtml",
    "content": "﻿@inject IUserRetrievalShim UserRetrievalShim\n@model CategorizedForumContainer\n@{\n\tViewBag.Title = Model.ForumTitle;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n@section HeaderContent{\n\t<link rel=\"sitemap\" href=\"@ViewBag.SitemapUrl\" type=\"application/xml\" />\n}\n\n<h1>@Model.ForumTitle</h1>\n\n@if (user != null)\n{\n\t@if (!user.IsApproved)\n\t{ <p class=\"alert alert-warning\"><a asp-controller=\"Account\" asp-action=\"Verify\">@PopForums.Resources.VerifyAccount</a></p> }\n\n\t<form asp-action=\"MarkAllForumsRead\" asp-controller=\"Forum\" asp-antiforgery=\"false\">\n\t\t<input type=\"submit\" value=\"@PopForums.Resources.MarkAllForumsRead\" class=\"btn btn-primary\" />\n\t</form>\n}\n\n<table class=\"table mt-3\">\n\t<tbody>\n\t@foreach (var forum in Model.UncategorizedForums)\n\t{\n\t\t<tr data-forumid=\"@forum.ForumID\">\n\t\t\t<td class=\"newIndicator\"><a asp-controller=\"Forum\" asp-action=\"Index\" asp-route-urlName=\"@forum.UrlName\"><pf-forumReadIndicator forum=\"@forum\" categorizedForumContainer=\"@Model\" /></a></td>\n\t\t\t<td class=\"w-100\">\n\t\t\t\t<h2><a asp-controller=\"Forum\" asp-action=\"Index\" asp-route-urlName=\"@forum.UrlName\">@forum.Title</a></h2>\n\t\t\t\t<p>@forum.Description</p>\n\t\t\t\t<small class=\"float-end text-secondary d-none d-sm-block\">\n\t\t\t\t\t@PopForums.Resources.Topics: <span class=\"topicCount\">@forum.TopicCount.ToString(\"N0\")</span> | @PopForums.Resources.Posts: <span class=\"postCount\">@forum.PostCount.ToString(\"N0\")</span> | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@forum.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By <span class=\"lastPostName\">@forum.LastPostName</span>\n\t\t\t\t</small>\n\t\t\t</td>\n\t\t</tr>\n\t}\n\t@foreach (var category in Model.CategoryDictionary)\n\t{\n\t\t<tr>\n\t\t\t<td colspan=\"2\" class=\"bg-primary text-light\">@category.Key.Title</td>\n\t\t</tr>\n\t\tforeach (var forum in category.Value)\n\t\t{\n\t\t\t<tr data-forumid=\"@forum.ForumID\">\n\t\t\t\t<td class=\"newIndicator\"><a asp-controller=\"Forum\" asp-action=\"Index\" asp-route-urlName=\"@forum.UrlName\"><pf-forumReadIndicator forum=\"@forum\" categorizedForumContainer=\"@Model\" /></a></td>\n\t\t\t\t<td class=\"w-100\">\n\t\t\t\t\t<h2><a asp-controller=\"Forum\" asp-action=\"Index\" asp-route-urlName=\"@forum.UrlName\">@forum.Title</a></h2>\n\t\t\t\t\t<p>@forum.Description</p>\n\t\t\t\t\t<small class=\"float-end text-secondary d-none d-sm-block\">\n\t\t\t\t\t\t@PopForums.Resources.Topics: <span class=\"topicCount\">@forum.TopicCount.ToString(\"N0\")</span> | @PopForums.Resources.Posts: <span class=\"postCount\">@forum.PostCount.ToString(\"N0\")</span> | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@forum.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By <span class=\"lastPostName\">@forum.LastPostName</span>\n\t\t\t\t\t</small>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t}\n\t}\n\t</tbody>\n</table>\n\n<pf-homeupdater></pf-homeupdater>\n\n<div>\n\t<h3>@PopForums.Resources.UsersOnline</h3>\n\t<p>\n\t\t@PopForums.Resources.Total: @ViewBag.TotalUsers@foreach (var u in (List<PopForums.Models.User>)ViewBag.OnlineUsers)\n\t\t{<text>, </text><a asp-controller=\"Account\" asp-action=\"ViewProfile\" asp-route-id=\"@u.UserID\">@u.Name</a>}\n\t</p>\n\t<p>@PopForums.Resources.TotalTopics: @ViewBag.TopicCount, @PopForums.Resources.TotalPosts: @ViewBag.PostCount, @PopForums.Resources.RegisteredUsers: @ViewBag.RegisteredUsers</p>\n</div>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Identity/ExternalError.cshtml",
    "content": "﻿@model string\n@{\n\tViewBag.Title = PopForums.Resources.Login + \" - \" + PopForums.Resources.Error;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<div>\n\t<h1>@PopForums.Resources.Login - @PopForums.Resources.Error</h1>\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"Home\" asp-action=\"Index\" asp-area=\"Forums\">@PopForums.Resources.Forums</a></li>\n\t</ul>\n</div>\n\n<div class=\"row\">\n\t<div class=\"col-12\">\n\t\t<p>@PopForums.Resources.Error: @Model</p>\n\t\t\n\t\t<p><a asp-controller=\"Account\" asp-action=\"Login\" asp-area=\"Forums\">@PopForums.Resources.Login</a></p>\n\t</div>\n</div>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Identity/ExternalLoginCallback.cshtml",
    "content": "﻿@{\n\tViewBag.Title = @PopForums.Resources.Login;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<div>\n\t<h1>@PopForums.Resources.Login - @PopForums.Resources.ExternalLogins</h1>\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t</ul>\n</div>\n\n<div class=\"row\">\n\t<div class=\"col-6\">\n\t\t<h2><a asp-controller=\"Account\" asp-action=\"Create\">@PopForums.Resources.CreateAnAccount</a></h2>\n\t</div>\n\t<div class=\"col-6\">\n\t\t<h2>@PopForums.Resources.UseExistingForumAccount</h2>\n\t\t<div id=\"LoginResult\" class=\"d-none alert alert-danger\"></div>\n        \n\t\t<template id=\"LoginForm\">\n\t\t<div role=\"form\">\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label for=\"EmailLogin\" class=\"form-label\">@PopForums.Resources.Email</label>\n\t\t\t\t<input type=\"text\" name=\"EmailLogin\" id=\"EmailLogin\" class=\"form-control\"/>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3\">\n\t\t\t\t<label for=\"PasswordLogin\" class=\"form-label\">@PopForums.Resources.Password</label>\n\t\t\t\t<input type=\"password\" name=\"PasswordLogin\" id=\"PasswordLogin\" class=\"form-control\" />\n\t\t\t</div>\n\t\t\t<input id=\"LoginButton\" type=\"button\" value=\"@PopForums.Resources.Login\" class=\"btn btn-primary mb-3\" />\n\t\t</div>\n\t\t</template>\n\n\t\t<pf-loginform templateid=\"LoginForm\" isexternallogin=\"true\"></pf-loginform>\n\t\t<input type=\"hidden\" name=\"Referrer\" id=\"Referrer\" value=\"@((object)ViewBag.Referrer)\" />\n\t</div>\n</div>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Ignore/List.cshtml",
    "content": "﻿@model IEnumerable<IgnoreWithName>\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tViewBag.Title = PopForums.Resources.IgnoreList;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n<h1>@PopForums.Resources.IgnoreList</h1>\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\" asp-route-pageNumber=\"\">@PopForums.Resources.Recent</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item active\">@PopForums.Resources.IgnoreList</li>\n</ul>\n\n@if (user == null)\n{\n\t<p>@PopForums.Resources.NotLoggedIn</p>\n}\nelse\n{\n\tif (Model == null || !Model.Any())\n\t{\n\t\t<p>@PopForums.Resources.NoResults</p>\n\t}\n\t\n\t<table class=\"table mt-3 w-auto table-borderless\">\n\t\t@foreach (var name in Model)\n\t\t{\n\t\t\t<tr>\n\t\t\t\t<td>\n\t\t\t\t\t<form method=\"post\" asp-action=\"RemoveFromList\" asp-route-id=\"@name.IgnoreUserID\">\n\t\t\t\t\t\t<button type=\"submit\" class=\"btn btn-primary\">@PopForums.Resources.Remove</button>\n\t\t\t\t\t</form>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"align-middle\">\n\t\t\t\t\t@name.Name\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t}\n\t</table>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Moderator/PostModerationLog.cshtml",
    "content": "﻿@model List<ModerationLogEntry>\n\n<div class=\"alert alert-secondary\">\n\t<table class=\"table\">\n\t\t@foreach (var entry in Model)\n\t\t{\n\t\t\t<tr>\n\t\t\t\t<td><pf-formattedtime utctime=\"@entry.TimeStamp.ToString(\"o\")\"></pf-formattedtime></td>\n\t\t\t\t<td><a asp-controller=\"@AccountController.Name\" asp-action=\"ViewProfile\" asp-route-id=\"@entry.UserID\" target=\"_blank\">@entry.UserName</a></td>\n\t\t\t\t<td>@entry.ModerationType</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t<td colspan=\"3\" style=\"border-bottom: solid 1px black;\">\n\t\t\t\t\t<p>@PopForums.Resources.Comment: @entry.Comment</p>\n\t\t\t\t\t@Html.Raw(entry.OldText)\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t}\n\t</table>\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Moderator/TopicModerationLog.cshtml",
    "content": "﻿@model List<ModerationLogEntry>\n\n<div class=\"alert alert-secondary\">\n\t<h3>@PopForums.Resources.ModerationLog</h3>\n\n\t@if (Model.Count == 0)\n\t{\n\t\t<p>@PopForums.Resources.None</p>\n\t}\n\t<table class=\"table\">\n\t\t@foreach (var entry in Model)\n\t\t{\n\t\t\t<tr>\n\t\t\t\t<td><pf-formattedtime utctime=\"@entry.TimeStamp.ToString(\"o\")\"></pf-formattedtime></td>\n\t\t\t\t@if (entry.UserID == 0)\n\t\t\t\t{\n\t\t\t\t\t<td>@entry.UserName</td>\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t<td><a asp-controller=\"@AccountController.Name\" asp-action=\"ViewProfile\" asp-route-id=\"@entry.UserID\" target=\"_blank\">@entry.UserName</a></td>\n\t\t\t\t}\n\t\t\t\t<td>@entry.ModerationType</td>\n\t\t\t</tr>\n\t\t}\n\t</table>\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/PrivateMessages/Archive.cshtml",
    "content": "﻿@inject IUserRetrievalShim UserRetrievalShim\n@model List<PrivateMessage>\n@{\n    var user = UserRetrievalShim.GetUser();\n\tViewBag.Title = PopForums.Resources.PrivateMessages + \" - \" + PopForums.Resources.Archived;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<div>\n\t<h1>@PopForums.Resources.Archived</h1>\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"Home\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t\t<li class=\"breadcrumb-item\"><a asp-action=\"Index\">@PopForums.Resources.PrivateMessages</a></li>\n\t</ul>\n</div>\n\n<pf-pagerlinks controllerName=\"PrivateMessages\" actionName=\"Archive\" pagerContext=\"@ViewBag.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n<table class=\"table mt-3 table-hover\">\n\t<tbody>\n\t@foreach (var pm in Model)\n\t{\n\t\t<tr>\n\t\t\t<td class=\"newIndicator\">\n\t\t\t\t<a asp-action=\"View\" asp-route-id=\"@pm.PMID\"><pf-pmReadIndicator privateMessage=\"@pm\"/></a>\n\t\t\t</td>\n\t\t\t<td>\n\t\t\t\t<form asp-action=\"UnarchivePM\" asp-route-id=\"@pm.PMID\" method=\"post\"><input type=\"submit\" value=\"@PopForums.Resources.Unarchive\" class=\"btn btn-primary\" /></form>\n\t\t\t</td>\n\t\t\t<td class=\"w-100\">\n\t\t\t\t<h2><a asp-action=\"View\" asp-route-id=\"@pm.PMID\">@PrivateMessage.GetUserNames(pm, user.UserID)</a></h2>\n\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\"><pf-formattedtime utctime=\"@pm.LastPostTime.ToString(\"o\")\"></pf-formattedtime></small>\n\t\t\t</td>\n\t\t</tr>\n\t}\n\t</tbody>\n</table>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/PrivateMessages/Create.cshtml",
    "content": "﻿@{\n\tViewBag.Title = PopForums.Resources.NewPM;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n@section HeaderContent\n{\n\t<script type=\"text/javascript\" language=\"javascript\">\n\tPopForums.Ready(() => {\n\t\tlet toText = document.querySelector(\"#ToText\");\n\t\tlet toList = document.querySelector(\"#ToList\");\n\t\ttoText.addEventListener(\"keyup\", () => {\n\t\t\tlet q = toText.value;\n\t\t\tif (q.length > 1) {\n\t\t\t\tfetch(PopForums.AreaPath + \"/PrivateMessages/GetNames?id=\" + q)\n\t\t\t\t\t.then(response => response.json())\n\t\t\t\t\t.then(result => {\n\t\t\t\t\t\ttoList.innerHTML = \"\";\n\t\t\t\t\t\tresult.forEach(item => {\n\t\t\t\t\t\t\tlet ids = document.getElementById(\"UserIDs\").value;\n\t\t\t\t\t\t\tlet idArray = ids.split(\",\");\n                            if (!idArray.includes(item.userID.toString()) && PopForums.userState.userID != item.userID.toString()) {\n\t\t\t\t\t\t\t\tlet t = document.createElement(\"template\");\n\t\t\t\t\t\t\t\tt.innerHTML = '<li><a href=\"#\" data-userID=\"' + item.userID + '\" class=\"toItem\">' + item.value + '</a></li>';\n\t\t\t\t\t\t\t\ttoList.append(t.content.firstChild);\n                            }\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t} else\n\t\t\t\ttoList.innerHTML = \"\";\n\t\t});\n\n\t\tdocument.addEventListener(\"click\", e => {\n\t\t\tif (event.target.classList.contains(\"toItem\")) {\n\t\t\t\tlet item = e.target;\n\t\t\t\tlet userID = item.getAttribute(\"data-userID\");\n\t\t\t\tlet ids = document.getElementById(\"UserIDs\").value;\n\t\t\t\tlet idArray = ids.split(\",\");\n\t\t\t\tif (idArray.includes(userID))\n\t\t\t\t\treturn;\n\t\t\t\tlet label = '<div data-userID=\"' + userID + '\" class=\"badge bg-primary toLabel\">' + item.innerHTML + ' <span class=\"icon icon-trash3-fill toLabelX\"></span></div>';\n\t\t\t\tlet t = document.createElement(\"template\");\n\t\t\t\tt.innerHTML = label;\n\t\t\t\tdocument.querySelector(\"#PMToBox\").append(t.content.firstChild);\n\t\t\t\ttoList.innerHTML = \"\";\n\t\t\t\ttoText.value = \"\";\n\t\t\t\tlet modal = bootstrap.Modal.getInstance(document.getElementById(\"ToModal\"));\n\t\t\t\tmodal.hide();\n\t\t\t\tserializeIDs();\n\t\t\t}\n\t\t});\n\n\t\tdocument.addEventListener(\"click\", e => {\n\t\t\tif (event.target.classList.contains(\"toLabelX\")) {\n\t\t\t\tevent.target.parentElement.remove();\n\t\t\t\tserializeIDs();\n\t\t\t}\n\t\t});\n\n\t\tlet modal = document.getElementById(\"ToModal\");\n\t\tmodal.addEventListener(\"shown.bs.modal\", () => {\n\t\t\tdocument.querySelector(\"#ToText\").focus();\n\t\t});\n\t\t\n\t\tserializeIDs();\n\n        let textBox = document.getElementById(\"NewMessage\");\n\t\ttextBox.addEventListener(\"keydown\", (e) => {\n\t\t\tif (e.key === \"Enter\" && !e.shiftKey) {\n\t\t\t\te.preventDefault();\n                    if (textBox.value.trim().length > 0 && document.querySelector(\"#UserIDs\").value.length > 0)\n\t\t\t\t\tdocument.forms[\"NewPMForm\"].submit();\n\t\t\t}\n\t\t});\n\t});\n\n\tfunction serializeIDs() {\n\t\tlet items = document.querySelectorAll(\"#PMToBox div\");\n\t\tlet ids = [];\n\t\titems.forEach(item => { ids.push(item.getAttribute(\"data-userID\")); });\n\t\tdocument.querySelector(\"#UserIDs\").value = ids;\n        let button = document.getElementById(\"ToButton\");\n        if (ids.length > 9)\n\t\t\tbutton.disabled = true;\n        else\n\t\t\tbutton.disabled = false;\n\t}\n</script>\n}\n\n<div>\n\t<h1>@PopForums.Resources.NewPM</h1>\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"Home\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t\t<li class=\"breadcrumb-item\"><a asp-action=\"Index\">@PopForums.Resources.PrivateMessages</a></li>\n\t</ul>\n</div>\n\n@if (ViewBag.Warning != null)\n{ <p class=\"alert alert-danger\">@ViewBag.Warning</p> }\n\n<div class=\"modal fade\" id=\"ToModal\" tabindex=\"-1\" role=\"dialog\">\n\t<div class=\"modal-dialog modal-sm\">\n\t\t<div class=\"modal-content\">\n\t\t\t<div class=\"modal-header\">\n\t\t\t\t<h4 class=\"modal-title\">@PopForums.Resources.To</h4>\n\t\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n\t\t\t</div>\n\t\t\t<div class=\"modal-body\">\n\t\t\t\t<input id=\"ToText\" type=\"text\" class=\"form-control\" placeholder=\"@PopForums.Resources.Search\" />\n\t\t\t\t<div id=\"ToResultList\">\n\t\t\t\t\t<ul id=\"ToList\" class=\"list-unstyled\"></ul>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"modal-footer\">\n\t\t\t\t<button type=\"button\" class=\"btn btn-primary\" data-bs-dismiss=\"modal\">Close</button>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>\n\n<form id=\"NewPMForm\" method=\"post\">\n\t<input type=\"hidden\" id=\"UserIDs\" name=\"UserIDs\"/>\n\t<div role=\"form\">\n\t\t<div class=\"mb-3\">\n            <label class=\"form-label\"><input type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#ToModal\" value=\"@PopForums.Resources.To\" id=\"ToButton\" /></label>\n\t\t\t<div id=\"PMToBox\">\n\t\t\t\t@if (ViewBag.TargetUserID != null)\n\t\t\t\t{\n                    <div data-userid=\"@ViewBag.TargetUserID\" class=\"badge bg-primary toLabel\">@ViewBag.TargetUserName <span class=\"icon icon-trash3-fill toLabelX\"></span></div>\n\t\t\t\t}\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"input-group mb-3\">\n            <textarea class=\"form-control\" id=\"NewMessage\" name=\"FullText\"></textarea>\n            <button class=\"btn btn-primary\" type=\"submit\">@PopForums.Resources.Send</button>\n\t\t</div>\n\t</div>\n</form>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/PrivateMessages/Index.cshtml",
    "content": "﻿@inject IUserRetrievalShim UserRetrievalShim\n@model List<PrivateMessage>\n@{\n\tvar user = UserRetrievalShim.GetUser();\n\tViewBag.Title = PopForums.Resources.PrivateMessages;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<div>\n\t<h1>@PopForums.Resources.PrivateMessages</h1>\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li><a asp-controller=\"Home\" asp-action=\"Index\" >@PopForums.Resources.Forums</a></li>\n\t</ul>\n</div>\n\n<p><a asp-action=\"Create\">@PopForums.Resources.SendPM</a></p>\n\n<pf-pagerlinks controllerName=\"PrivateMessages\" actionName=\"Index\" pagerContext=\"@ViewBag.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\"/>\n\n<table class=\"table mt-3 table-hover\">\n\t<tbody>\n\t@foreach (var pm in Model)\n\t{\n\t\t<tr>\n\t\t\t<td class=\"newIndicator\">\n\t\t\t\t<a asp-action=\"View\" asp-route-id=\"@pm.PMID\"><pf-pmReadIndicator privateMessage=\"@pm\"/></a>\n\t\t\t</td>\n\t\t\t<td>\n\t\t\t\t<form asp-action=\"ArchivePM\" asp-route-id=\"@pm.PMID\"><input type=\"submit\" value=\"@PopForums.Resources.Archive\" class=\"btn btn-primary\"/></form>\n\t\t\t</td>\n\t\t\t<td class=\"w-100\">\n\t\t\t\t<h2><a asp-action=\"View\" asp-route-id=\"@pm.PMID\">@PrivateMessage.GetUserNames(pm, user.UserID)</a></h2>\n\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\"><pf-formattedtime utctime=\"@pm.LastPostTime.ToString(\"o\")\"></pf-formattedtime></small>\n\t\t\t</td>\n\t\t</tr>\n\t}\n\t</tbody>\n</table>\n<p><a asp-action=\"Archive\">@PopForums.Resources.ViewArchivedMessages</a></p>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/PrivateMessages/View.cshtml",
    "content": "﻿@inject IUserRetrievalShim UserRetrievalShim\n@using PopForums\n@model PrivateMessageView\n@{\n\tvar user = UserRetrievalShim.GetUser();\n\tvar title = PrivateMessage.GetUserNames(Model.PrivateMessage, user.UserID);\n\tViewBag.Title = PopForums.Resources.PrivateMessages + \" - \" + title;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n    var pmSerialized = JsonSerializer.Serialize(Model.State, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n}\n\n@section HeaderContent {\n\t<script type=\"text/javascript\">\n        PopForums.currentPmState = Object.assign(new PopForums.PrivateMessageState(), @Html.Raw(pmSerialized));\n        PopForums.currentPmState.setupPm();\n\t</script>\n}\n\n<div class=\"vh-100 d-flex flex-column mb-3\">\n\n<div>\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li class=\"breadcrumb-item\"><a asp-controller=\"Home\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t\t<li class=\"breadcrumb-item\"><a asp-action=\"Index\">@PopForums.Resources.PrivateMessages</a></li>\n\t</ul>\n\t<h2>@title</h2>\n</div>\n\n<div class=\"overflow-auto my-3 px-3 border border-2 rounded\">\n    <div id=\"PostStream\"></div>\n</div>\n\n\t<div>\n\t\t@if (Model.State.IsUserNotFound)\n\t\t{\n\t\t\t<p>@Resources.UserNotFound</p>\n\t\t}\n\t\telse\n\t\t{\n\t\t\t<pf-pmform></pf-pmform>\n\t\t}\n\n\t\t<form asp-action=\"ArchivePM\" asp-route-id=\"@Model.PrivateMessage.PMID\">\n\t\t\t<p><input type=\"submit\" value=\"@PopForums.Resources.Archive\" class=\"btn btn-primary\"/></p>\n\t\t</form>\n\t</div>\n\n</div>\n"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Search/Index.cshtml",
    "content": "﻿@model PagedTopicContainer\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tViewBag.Title = PopForums.Resources.Search;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar routeParameters = new Dictionary<string, object> { { \"query\", ViewBag.Query }, { \"searchType\", ViewBag.SearchType } };\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n<h1>@PopForums.Resources.Search</h1>\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\" asp-route-pageNumber=\"\">@PopForums.Resources.Recent</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item active\">@PopForums.Resources.Search</li>\n</ul>\n\n<div id=\"SearchForm\">\n\t<form asp-action=\"Process\" method=\"post\" role=\"form\" class=\"mb-4\" asp-antiforgery=\"false\">\n\t\t<div class=\"row mb-3\">\n\t\t\t<div class=\"col-7\">\n\t\t\t\t<input type=\"text\" name=\"Query\" id=\"Query\" class=\"form-control me-1\" value=\"@(ViewBag.Query)\" />\n\t\t\t</div>\n\t\t\t<div class=\"col-3\">\n\t\t\t\t@Html.DropDownList(\"SearchType\", (SelectList)ViewBag.SearchTypes, new { @class = \"form-select me-1 \" })\n\t\t\t</div>\n\t\t\t<div class=\"col-2\">\n\t\t\t\t<input type=\"submit\" value=\"@PopForums.Resources.Search\" class=\"btn btn-primary\" />\n\t\t\t</div>\n\t\t</div>\n\t</form>\n</div>\n\n<pf-pagerlinks actionName=\"Result\" controllerName=\"Search\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" routeParameters=\"@routeParameters\" />\n\n@if ((bool) ViewBag.IsError)\n{\n\t<div class=\"alert alert-danger\">@PopForums.Resources.SearchError</div>\n}\n\n@if (Model.Topics.Count == 0 && !String.IsNullOrEmpty(ViewBag.Query))\n{\n\t<p>@PopForums.Resources.NoResults</p>\n}\nelse\n{\n\t<table id=\"TopicList\" class=\"table mt-3 table-hover\">\n\t\t@foreach (var topic in Model.Topics)\n\t\t{\n\t\t\t<tr @if (topic.IsDeleted) { <text> class=\"bg-warning\" </text>   }>\n\t\t\t\t<td class=\"newIndicator\">\n\t\t\t\t\t@if (user == null)\n\t\t\t\t\t{\n\t\t\t\t\t\t<a asp-controller=\"Forum\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\" /></a>\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t<a asp-controller=\"@ForumController.Name\" asp-action=\"GoToNewestPost\" asp-route-id=\"@topic.TopicID\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\" /></a>\n\t\t\t\t\t}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"w-100\">\n\t\t\t\t\t<h2>@Html.ActionLink(topic.Title, \"Topic\", \"Forum\", new { id = topic.UrlName, pageNumber = 1 }, null)</h2>\n\t\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t\t\t\t@PopForums.Resources.StartedBy: @topic.StartedByName @PopForums.Resources.In @Model.ForumTitles[topic.ForumID] | @PopForums.Resources.Views: @topic.ViewCount | @PopForums.Resources.Replies: @topic.ReplyCount | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@topic.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By @topic.LastPostName\n\t\t\t\t\t</small>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t}\n\t</table>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Setup/Exception.cshtml",
    "content": "﻿@model System.Exception\n<!DOCTYPE html>\n\n<html>\n<head>\n\t<title>@PopForums.Resources.PopForumsSetup - @PopForums.Resources.Error</title>\n    <link href=\"~/PopForums/lib/bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" type=\"text/css\" />\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<p>@PopForums.Resources.ErrorSettingUpDb:</p>\n\n\t\t<h2>@Model.Message</h2>\n\n\t\t<p>@Html.Raw(Model.StackTrace.Replace(Environment.NewLine, \"<br />\"))</p>\n\t</div>\n</body>\n</html>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Setup/Index.cshtml",
    "content": "﻿@model SetupVariables\n\n<!DOCTYPE html>\n\n<html>\n<head>\n\t<title>@PopForums.Resources.PopForumsSetup</title>\n    <link href=\"~/PopForums/lib/bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" />\n\t<script type=\"text/javascript\">\n\t\tvar ready = (callback) => {\n\t\t\tif (document.readyState != \"loading\") callback();\n\t\t\telse document.addEventListener(\"DOMContentLoaded\", callback);\n\t\t};\n\n\t\tready(() => {\n\t\t\tvar useEsmtp = document.querySelector(\"#UseEsmtp\");\n\t\t\tSetEsmtpBoxes();\n\t\t\tuseEsmtp.addEventListener(\"click\", () => { SetEsmtpBoxes(); });\n\t\t});\n\n\t\tfunction SetEsmtpBoxes() {\n\t\t\tvar enabled = document.querySelector(\"#UseEsmtp\").checked;\n\t\t\tif (enabled)\n\t\t\t\tdocument.querySelectorAll(\"#SmtpUser, #SmtpPassword\").forEach(x => x.removeAttribute(\"disabled\"));\n\t\t\telse\n\t\t\t\tdocument.querySelectorAll(\"#SmtpUser, #SmtpPassword\").forEach(x => x.setAttribute(\"disabled\", \"disabled\"));\n\t\t}\n\t</script>\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<h1>@PopForums.Resources.PopForumsSetup</h1>\n\t\t<p>@PopForums.Resources.SetupConnSuccess:</p>\n\t\t@using (Html.BeginForm(\"Index\", \"Setup\", FormMethod.Post, new { @class = \"form-horizontal\", role = \"form\" }))\n\t\t{\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"ForumTitle\">@PopForums.Resources.ForumTitle</label>\n\t\t\t\t<div class=\"col-10\">@Html.TextBoxFor(s => s.ForumTitle, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"SmtpServer\">@PopForums.Resources.SmtpServer</label>\n\t\t\t\t<div class=\"col-10\">@Html.TextBoxFor(s => s.SmtpServer, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"SmtpPort\">@PopForums.Resources.SmtpPort</label>\n\t\t\t\t<div class=\"col-2\">@Html.TextBoxFor(s => s.SmtpPort, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"MailerAddress\">@PopForums.Resources.FromEmailAddress</label>\n\t\t\t\t<div class=\"col-10\">@Html.TextBoxFor(s => s.MailerAddress, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"offset-2 col-10\">\n\t\t\t\t\t<div class=\"form-check\">\n\t\t\t\t\t\t@Html.CheckBoxFor(s => s.UseSslSmtp, new { @class = \"form-check-input\" })\n\t\t\t\t\t\t<label class=\"form-check-label\" for=\"UseSslSmtp\">@PopForums.Resources.UseSsl</label>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<div class=\"offset-2 col-10\">\n\t\t\t\t\t<div class=\"form-check\">\n\t\t\t\t\t\t@Html.CheckBoxFor(s => s.UseEsmtp, new { @class = \"form-check-input\" })\n\t\t\t\t\t\t<label class=\"form-check-label\" for=\"UseEsmtp\">@PopForums.Resources.UseEsmtpCred</label>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"SmtpUser\">@PopForums.Resources.SmtpUser</label>\n\t\t\t\t<div class=\"col-10\">@Html.TextBoxFor(s => s.SmtpUser, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"SmtpPassword\">@PopForums.Resources.SmtpPassword</label>\n\t\t\t\t<div class=\"col-10\">@Html.TextBoxFor(s => s.SmtpPassword, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<h3>@PopForums.Resources.SetupFirstUser</h3>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"Name\">@PopForums.Resources.DisplayName</label>\n\t\t\t\t<div class=\"col-10\">@Html.TextBoxFor(s => s.Name, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"Email\">@PopForums.Resources.Email</label>\n\t\t\t\t<div class=\"col-10\">@Html.TextBoxFor(s => s.Email, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<label class=\"col-2 form-label\" for=\"Password\">@PopForums.Resources.Password</label>\n\t\t\t\t<div class=\"col-10\">@Html.PasswordFor(s => s.Password, new { @class = \"form-control\" })</div>\n\t\t\t</div>\n\t\t\t<div class=\"mb-3 row\">\n\t\t\t\t<input type=\"submit\" value=\"@PopForums.Resources.SetupDatabase\" class=\"btn btn-primary\" />\n\t\t\t</div>\n\t\t}\n\t</div>\n</body>\n</html>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Setup/NoConnection.cshtml",
    "content": "﻿<!DOCTYPE html>\n\n<html>\n<head>\n\t<title>@PopForums.Resources.PopForumsSetup - @PopForums.Resources.NoDataConnection</title>\n    <link href=\"~/PopForums/lib/bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" type=\"text/css\" />\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<h1>@PopForums.Resources.PopForumsSetup</h1>\n\n\t\t<p>@PopForums.Resources.SetupCantConnect</p>\n\t</div>\n</body>\n</html>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Setup/Success.cshtml",
    "content": "﻿<!DOCTYPE html>\n\n<html>\n<head>\n\t<title>@PopForums.Resources.PopForumsSetup - @PopForums.Resources.Success</title>\n    <link href=\"~/PopForums/lib/bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" type=\"text/css\" />\n</head>\n<body>\n\t<div class=\"container\">\n\t\t<h1>@PopForums.Resources.PopForumsSetup -  @PopForums.Resources.Success</h1>\n\t\t<p>Congratulations!</p>\n\t\t<p>@PopForums.Resources.ForumReady</p>\n\t\t<p>@PopForums.Resources.AppRestartRequired</p>\n\t</div>\n</body>\n</html>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Shared/Components/UserNavigation/Default.cshtml",
    "content": "﻿@using PopForums.Configuration\n@inject IUserRetrievalShim UserRetrievalShim\n@inject ISettingsManager SettingsManager\n@inject IConfig Config\n@{\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n<template id=\"SearchNavTemplate\">\n\t<div class=\"nav-item dropdown\">\n\t\t<a class=\"nav-link p-2\" data-bs-toggle=\"dropdown\" href=\"#\" id=\"SearchDropDown\" title=\"@PopForums.Resources.Search\"><span class=\"icon icon-search\"></span></a>\n\t\t<div class=\"dropdown-menu dropdown-menu-end\" role=\"menu\">\n\t\t\t<form asp-controller=\"Search\" asp-action=\"Result\" asp-antiforgery=\"false\" method=\"post\" class=\"\" role=\"form\">\n\t\t\t\t<input type=\"search\" class=\"form-control w-auto mx-2\" placeholder=\"@PopForums.Resources.Search\" id=\"query\" name=\"query\">\n\t\t\t</form>\n\t\t</div>\n\t</div>\n</template>\n\n<div class=\"row mb-3\">\n\t<nav class=\"navbar navbar-expand bg-light px-3\">\n\t\t<div class=\"d-flex order-2\">\n\t\t\t<pf-searchnavform templateid=\"SearchNavTemplate\" textboxid=\"query\" dropdownid=\"SearchDropDown\"></pf-searchnavform>\n\t\t</div>\n\t\t<div id=\"AccountBox\" class=\"ms-auto\">\n\t\t\t<ul class=\"navbar-nav ms-auto\">\n\t\t\t\t@if (user == null)\n\t\t\t\t{\n\t\t\t\t\t<li class=\"nav-item\"><a asp-controller=\"@AccountController.Name\" asp-action=\"Create\" asp-route-area=\"Forums\" class=\"nav-link\">@PopForums.Resources.CreateAnAccount</a></li>\n\t\t\t\t\t<li class=\"nav-item\"><a asp-controller=\"@AccountController.Name\" asp-action=\"Login\" asp-route-area=\"Forums\" class=\"nav-link\">@PopForums.Resources.Login</a></li>\n\t\t\t\t\tif (!SettingsManager.Current.IsPrivateForumInstance)\n\t\t\t\t\t{\n\t\t\t\t\t\t<li class=\"nav-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\" asp-route-pageNumber=\"\" asp-route-area=\"Forums\" class=\"nav-link\" title=\"@PopForums.Resources.Recent\"><span class=\"icon icon-clock-history\"></span></a></li>\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t<li class=\"nav-item dropdown\">\n\t\t\t\t\t\t<a class=\"nav-link dropdown-toggle\" data-bs-toggle=\"dropdown\" href=\"#\" title=\"@user.Name\"><span class=\"icon icon-person-fill\"></span></a>\n\t\t\t\t\t\t<div class=\"dropdown-menu\" role=\"menu\">\n\t\t\t\t\t\t\t<a class=\"dropdown-item disabled\">@user.Name</a>\n\t\t\t\t\t\t\t<a asp-controller=\"@AccountController.Name\" asp-action=\"ViewProfile\" asp-route-id=\"@user.UserID\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.Profile</a>\n\t\t\t\t\t\t\t<a asp-controller=\"@SubscriptionController.Name\" asp-action=\"Topics\" asp-route-pageNumber=\"\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.Subscriptions</a>\n\t\t\t\t\t\t\t<a asp-controller=\"@FavoritesController.Name\" asp-action=\"Topics\" asp-route-pageNumber=\"\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.Favorites</a>\n\t\t\t\t\t\t\t<a asp-controller=\"@IgnoreController.Name\" asp-action=\"List\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.IgnoreList</a>\n\t\t\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t\t\t<a asp-controller=\"@AccountController.Name\" asp-action=\"EditProfile\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.EditProfile</a>\n\t\t\t\t\t\t\t@if (!Config.IsOAuthOnly)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t<a asp-controller=\"@AccountController.Name\" asp-action=\"Security\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.ChangeYourEmailPassword</a>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t<a asp-controller=\"@AccountController.Name\" asp-action=\"ManagePhotos\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.ManageYourPhotos</a>\n\t\t\t\t\t\t\t@if (!Config.IsOAuthOnly)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t<a asp-controller=\"@AccountController.Name\" asp-action=\"ExternalLogins\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.ExternalLogins</a>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t@if (user.IsInRole(PermanentRoles.Admin))\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t\t\t\t<a asp-controller=\"@AdminController.Name\" asp-action=\"App\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.PopForumsAdmin</a>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t<div class=\"dropdown-divider\"></div>\n\t\t\t\t\t\t\t<a asp-controller=\"@IdentityController.Name\" asp-action=\"Logout\" asp-route-area=\"Forums\" class=\"dropdown-item\">@PopForums.Resources.Logout</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</li>\n\t\t\t\t\t<li class=\"nav-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\" asp-route-pageNumber=\"\" asp-route-area=\"Forums\" class=\"nav-link\" title=\"@PopForums.Resources.Recent\"><span class=\"icon icon-clock-history\"></span></a></li>\n\t\t\t\t\t<li class=\"nav-item\"><a class=\"nav-link\" asp-controller=\"@PrivateMessagesController.Name\" asp-action=\"Index\" asp-route-pageNumber=\"\" asp-route-area=\"Forums\" title=\"@PopForums.Resources.PrivateMessages\"><span class=\"icon icon-envelope\"></span> <pf-pmcount></pf-pmcount></a></li>\n\t\t\t\t\t<li class=\"nav-link\"><pf-notificationtoggle class=\"nav-item\" panelid=\"NotificationBar\" notificationlistid=\"NotificationList\"></pf-notificationtoggle></li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t</nav>\n</div>\n\n@if (user != null)\n{\n\t<div class=\"offcanvas offcanvas-end\" tabindex=\"-1\" id=\"NotificationBar\">\n\t\t<div class=\"offcanvas-header\">\n\t\t\t<pf-notificationmarkallbutton buttontext=\"@PopForums.Resources.MarkAllRead\" buttonclass=\"btn btn-primary\"></pf-notificationmarkallbutton>\n\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"offcanvas\" aria-label=\"Close\"></button>\n\t\t</div>\n\t\t<div class=\"offcanvas-body\" id=\"NotificationList\">\n\t\t\t<pf-notificationlist></pf-notificationlist>\n            <div id=\"NotificationBottom\"></div>\n\t\t</div>\n\t</div>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Shared/Components/UserState/Default.cshtml",
    "content": "﻿@model UserState\n@inject IUserRetrievalShim UserRetrievalShim\n@inject ITimeFormatStringService TimeFormatStringService\n@{\n\tvar serialized = JsonSerializer.Serialize(Model, new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n\tvar user = UserRetrievalShim.GetUser();\n}\n<script>\n\tPopForums.LocalizationService.init();\n\t@if (user != null)\n{<text>\n\tPopForums.userState = Object.assign(new PopForums.UserState(), @Html.Raw(serialized));\n    PopForums.userState.initialize();\n</text>}\n</script>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Shared/Forbidden.cshtml",
    "content": "﻿@{\n\tViewBag.Title = PopForums.Resources.Forbidden;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<div>\n\t<h1>@PopForums.Resources.Forbidden</h1>\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li>@Html.ActionLink(PopForums.Resources.Forums, \"Index\", HomeController.Name, null, null)</li>\n\t</ul>\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Shared/NotFound.cshtml",
    "content": "﻿@{\n\tViewBag.Title = PopForums.Resources.PageNotFound;\n    Layout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n}\n\n<div>\n\t<h1>@PopForums.Resources.PageNotFound</h1>\n\t<ul id=\"TopBreadcrumb\" class=\"breadcrumb\">\n\t\t<li>@Html.ActionLink(PopForums.Resources.Forums, \"Index\", HomeController.Name, null, null)</li>\n\t</ul>\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Shared/PopForumsMaster.cshtml",
    "content": "﻿@inject IConfig Config\n@{\n\tLayout = \"~/Views/Shared/_Layout.cshtml\";\n}\n\n@section HeaderContent {\n\t<environment include=\"Development\">\n    @if (Config.RenderBootstrap)\n    {\n            <script src=\"~/PopForums/lib/bootstrap/dist/js/bootstrap.bundle.js\" asp-append-version=\"true\"></script>\n            <link href=\"~/PopForums/lib/bootstrap/dist/css/bootstrap.css\" rel=\"stylesheet\" asp-append-version=\"true\" />\n    }\n        <script src=\"~/PopForums/lib/signalr/dist/signalr.js\" asp-append-version=\"true\"></script>\n        <script src=\"~/PopForums/PopForums.js\" asp-append-version=\"true\"></script>\n        <link href=\"~/PopForums/PopForums.css\" rel=\"stylesheet\" asp-append-version=\"true\" />\n\t</environment>\n\t<environment exclude=\"Development\">\n    @if (Config.RenderBootstrap)\n    {\n            <script src=\"~/PopForums/lib/bootstrap/dist/js/bootstrap.bundle.min.js\" asp-append-version=\"true\"></script>\n            <link href=\"~/PopForums/lib/bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" asp-append-version=\"true\" />\n    }\n        <script src=\"~/PopForums/lib/signalr/dist/signalr.min.js\" asp-append-version=\"true\"></script>\n        <script src=\"~/PopForums/lib/PopForums/dist/PopForums.min.js\" asp-append-version=\"true\"></script>\n        <link href=\"~/PopForums/lib/PopForums/dist/PopForums.min.css\" rel=\"stylesheet\" asp-append-version=\"true\" />\n\t</environment>\n\t@await RenderSectionAsync(\"HeaderContent\", required: false)\n\t@await Component.InvokeAsync(\"UserState\")\n}\n\n<div id=\"ForumContainer\" class=\"container-fluid\">\n\n\t@await Component.InvokeAsync(\"UserNavigation\")\n\n\t<div class=\"row\">\n\t\t<div class=\"col-12\">\n\t\t\t@RenderBody()\n\t\t</div>\n\t</div>\n\n\t<div class=\"row\">\n\t\t<div id=\"ForumFooter\" class=\"col-12 mt-3\">\n\t\t\t<small><a href=\"https://popforums.com/\">POP Forums</a> - &copy;@DateTime.Now.Year, POP World Media, LLC</small>\n\t\t</div>\n\t</div>\n</div>"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/Subscription/Topics.cshtml",
    "content": "﻿@model PagedTopicContainer\n@inject IUserRetrievalShim UserRetrievalShim\n@{\n\tViewBag.Title = PopForums.Resources.SubscribedTopics;\n\tLayout = \"~/Areas/Forums/Views/Shared/PopForumsMaster.cshtml\";\n\tvar user = UserRetrievalShim.GetUser();\n}\n\n<h1>@PopForums.Resources.SubscribedTopics</h1>\n\n<ul id=\"TopBreadcrumb\" class=\"breadcrumb sticky-top\">\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@ForumController.Name\" asp-action=\"Recent\" asp-route-pageNumber=\"\">@PopForums.Resources.Recent</a></li>\n\t<li class=\"breadcrumb-item\"><a asp-controller=\"@HomeController.Name\" asp-action=\"Index\">@PopForums.Resources.Forums</a></li>\n\t<li class=\"breadcrumb-item active\">@PopForums.Resources.SubscribedTopics</li>\n</ul>\n\n@if (Model == null)\n{\n\t<p>@PopForums.Resources.SubscribeLoggedIn</p>\n}\nelse\n{\n\tif (Model.Topics.Count == 0)\n\t{\n\t\t<p>@PopForums.Resources.SubscribeNone</p>\n\t}\n\t\n\t<pf-pagerLinks controllerName=\"Subscription\" actionName=\"Topics\" pagerContext=\"@Model.PagerContext\" class=\"pagination pagination-sm pagerLinks\" moreTextClass=\"morePager\" currentTextClass=\"currentPager active\" />\n\n\t<table id=\"TopicList\" class=\"table mt-3 table-hover\">\n\t\t@foreach (var topic in Model.Topics)\n\t\t{\n\t\t\t<tr @if (topic.IsDeleted) { <text> class=\"bg-warning\" </text>   }>\n\t\t\t\t<td class=\"newIndicator\"><a asp-controller=\"@ForumController.Name\" asp-action=\"GoToNewestPost\" asp-route-id=\"@topic.TopicID\"><pf-topicReadIndicator topic=\"@topic\" pagedTopicContainer=\"@Model\" /></a></td>\n\t\t\t\t<td class=\"text-center\">\n\t\t\t\t\t<form asp-action=\"Unsubscribe\" asp-controller=\"Subscription\" asp-antiforgery=\"false\">\n\t\t\t\t\t\t<input name=\"id\" id=\"id\" type=\"hidden\" value=\"@topic.TopicID\" />\n\t\t\t\t\t\t<button type=\"submit\" class=\"btn btn-primary\"><span class=\"icon icon-bell-fill text-warning\"></span> @PopForums.Resources.Unsubscribe</button>\n\t\t\t\t\t</form>\n\t\t\t\t</td>\n\t\t\t\t<td>\n\t\t\t\t\t<h2><a asp-controller=\"@ForumController.Name\" asp-action=\"Topic\" asp-route-id=\"@topic.UrlName\" asp-route-pageNumber=\"\">@topic.Title</a></h2>\n\t\t\t\t\t<small class=\"float-end d-none d-sm-block text-secondary\">\n\t\t\t\t\t\t@PopForums.Resources.StartedBy: @topic.StartedByName @PopForums.Resources.In @Model.ForumTitles[topic.ForumID] | @PopForums.Resources.Views: @topic.ViewCount | @PopForums.Resources.Replies: @topic.ReplyCount | @PopForums.Resources.Last: <pf-formattedtime utctime=\"@topic.LastPostTime.ToString(\"o\")\"></pf-formattedtime> @PopForums.Resources.By @topic.LastPostName\n\t\t\t\t\t</small>\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t}\n\t</table>\n}"
  },
  {
    "path": "src/PopForums.Mvc/Areas/Forums/Views/_ViewImports.cshtml",
    "content": "﻿@using PopForums.Extensions\n@using PopForums.Models\n@using PopForums.Services\n@using PopForums.Mvc.Areas.Forums.Controllers\n@using PopForums.Mvc.Areas.Forums.Models\n@using PopForums.Mvc.Areas.Forums.Services\n@using System.Threading.Tasks\n@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers\n@addTagHelper *, PopForums.Mvc"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/AnswerButton.ts",
    "content": "namespace PopForums {\n\n    export class AnswerButton extends ElementBase<TopicState> {\n        constructor() {\n            super();\n        }\n\n        get answerstatusclass(): string {\n            return this.getAttribute(\"answerstatusclass\")!;\n        }\n        get chooseanswertext(): string {\n            return this.getAttribute(\"chooseanswertext\")!;\n        }\n        get topicid(): string {\n            return this.getAttribute(\"topicid\")!;\n        }\n        get postid(): string {\n            return this.getAttribute(\"postid\")!;\n        }\n        get answerpostid(): string {\n            return this.getAttribute(\"answerpostid\")!;\n        }\n        get userid(): string {\n            return this.getAttribute(\"userid\")!;\n        }\n        get startedbyuserid(): string {\n            return this.getAttribute(\"startedbyuserid\")!;\n        }\n        get isfirstintopic(): string {\n            return this.getAttribute(\"isfirstintopic\")!;\n        }\n\n        private button!: HTMLElement;\n\n        connectedCallback() {\n            this.button = document.createElement(\"p\");\n            this.button.classList.add(\"icon\");\n            this.answerstatusclass.split(\" \").forEach((c) => this.button.classList.add(c));\n            if (this.isfirstintopic.toLowerCase() === \"false\" && this.userid === this.startedbyuserid) {\n                // make it a button for author\n                this.button.addEventListener(\"click\", () => {\n                    PopForums.currentTopicState.setAnswer(Number(this.postid), Number(this.topicid));\n                });\n            }\n            this.appendChild(this.button);\n            super.connectedCallback();\n        }\n\n        getDependentReference(): [TopicState, string] {\n            return [PopForums.currentTopicState, \"answerPostID\"];\n        }\n\n        updateUI(answerPostID: number): void {\n            if (this.isfirstintopic.toLowerCase() === \"false\" && this.userid === this.startedbyuserid) {\n                // this is question author\n                this.button.classList.add(\"asnswerButton\", \"fs-1\", \"my-3\");\n                if (answerPostID && this.postid === answerPostID.toString()) {\n                    this.button.classList.remove(\"icon-check-circle\");\n                    this.button.classList.remove(\"text-muted\");\n                    this.button.classList.add(\"icon-check-circle-fill\");\n                    this.button.classList.add(\"text-success\");\n                    this.style.cursor = \"default\";\n                }\n                else {\n                    this.button.classList.remove(\"icon-check-circle-fill\");\n                    this.button.classList.remove(\"text-success\");\n                    this.button.classList.add(\"icon-check-circle\");\n                    this.button.classList.add(\"text-muted\");\n                    this.style.cursor = \"pointer\";\n                }\n            }\n            else if (answerPostID && this.postid === answerPostID.toString()) {\n                // not the question author, but it is the answer\n                this.button.classList.add(\"icon-check-circle-fill\");\n                this.button.classList.add(\"text-success\");\n                this.style.cursor = \"default\";\n            }\n        }\n}\n\ncustomElements.define('pf-answerbutton', AnswerButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/CommentButton.ts",
    "content": "namespace PopForums {\n\n    export class CommentButton extends ElementBase<TopicState> {\n        constructor() {\n            super();\n        }\n\n        get buttonclass(): string {\n            return this.getAttribute(\"buttonclass\")!;\n        }\n\n        get buttontext(): string {\n            return this.getAttribute(\"buttontext\")!;\n        }\n\n        get topicid(): string {\n            return this.getAttribute(\"topicid\")!;\n        }\n\n        get postid(): string {\n            return this.getAttribute(\"postid\")!;\n        }\n\n        connectedCallback() {\n            this.innerHTML = CommentButton.template;\n            let button = this.querySelector(\"button\") as HTMLButtonElement;\n            button.title = this.buttontext;\n            if (this.buttonclass?.length > 0)\n                this.buttonclass.split(\" \").forEach((c) => button.classList.add(c));\n            if (button.classList.contains(\"btn\"))\n                button.innerText = this.buttontext;\n            button.addEventListener(\"click\", (e: MouseEvent) => {\n                PopForums.currentTopicState.loadComment(Number(this.topicid), Number(this.postid));\n            });\n            super.connectedCallback();\n        }\n\n        getDependentReference(): [TopicState, string] {\n            return [PopForums.currentTopicState, \"commentReplyID\"];\n        }\n\n        updateUI(data: number): void {\n            let button = this.querySelector(\"button\")!;\n            if (data !== undefined) {\n                button.disabled = true;\n                button.style.cursor = \"default\";\n            }\n            else\n                button.disabled = false;\n        }\n\n        static template: string = `<button type=\"button\"></button>`;\n}\n\ncustomElements.define('pf-commentbutton', CommentButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/FavoriteButton.ts",
    "content": "namespace PopForums {\n\n    export class FavoriteButton extends ElementBase<TopicState> {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n\n    get makefavoritetext(): string {\n        return this.getAttribute(\"makefavoritetext\")!;\n    }\n    get removefavoritetext(): string {\n        return this.getAttribute(\"removefavoritetext\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = SubscribeButton.template;\n        let button: HTMLButtonElement = this.querySelector(\"button\")!;\n        this.buttonclass.split(\" \").forEach((c) => button.classList.add(c));\n        button.addEventListener(\"click\", () => {\n            fetch(PopForums.AreaPath + \"/Favorites/ToggleFavorite/\" + PopForums.currentTopicState.topicID, {\n                method: \"POST\"\n            })\n                .then(response => response.json())\n                .then(result => {\n                    switch (result.data.isFavorite) {\n                        case true:\n                            PopForums.currentTopicState.isFavorite = true;\n                            break;\n                        case false:\n                            PopForums.currentTopicState.isFavorite = false;\n                            break;\n                        default:\n                            // TODO: something else\n                    }\n                })\n                .catch(() => {\n                    // TODO: handle error\n                });\n        });\n        super.connectedCallback();\n    }\n\n    getDependentReference(): [TopicState, string] {\n        return [PopForums.currentTopicState, \"isFavorite\"];\n    }\n\n    updateUI(data: boolean): void {\n        let button = this.querySelector(\"button\")!;\n        if (data) {\n            button.title = this.removefavoritetext;\n            button.classList.remove(\"icon-star\", \"text-muted\");\n            button.classList.add(\"icon-star-fill\", \"text-warning\");\n        }\n        else {\n            button.title = this.makefavoritetext;\n            button.classList.remove(\"icon-star-fill\", \"text-warning\");\n            button.classList.add(\"icon-star\", \"text-muted\");\n        }\n    }\n\n    static template: string = `<button type=\"button\" class=\"btn-link icon\"></button>`;\n}\n\ncustomElements.define('pf-favoritebutton', FavoriteButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/FormattedTime.ts",
    "content": "namespace PopForums {\n\n    export class FormattedTime extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    get utctime(): string {\n        return this.getAttribute(\"utctime\")!;\n    }\n\n    private utcTime!: number;\n    private utcTimeAsDate!: Date;\n    private static dayInMs = 86400000;\n    private isReady!: boolean;\n\n    connectedCallback() {\n        const delegate = this.ready.bind(this);\n        this.isReady = LocalizationService.subscribe(delegate);\n        if (this.isReady)\n            this.ready();\n    }\n\n    ready() {\n        this.setBaseTime();\n        let now = Date.now();\n        let yesterdayMs = now - FormattedTime.dayInMs;\n        let yesterdayTemp = new Date(yesterdayMs);\n        let yesterday = new Date(yesterdayTemp.getFullYear(), yesterdayTemp.getMonth(), yesterdayTemp.getDate());\n        this.innerHTML = this.GetDisplayTime();\n        if (this.utcTime > yesterday.getTime())\n            this.UpdateTimer();\n        this.isReady = true;\n    }\n\n    private setBaseTime() {\n        let baseTime = this.utctime;\n        if (!baseTime.endsWith(\"Z\"))\n            baseTime = baseTime + \"Z\";\n        this.utcTime = Date.parse(baseTime);\n        this.utcTimeAsDate = new Date(baseTime);\n    }\n\n    private UpdateTimer(): void {\n        setTimeout(() => {\n            this.UpdateTimer();\n            this.innerHTML = this.GetDisplayTime();\n        }, 60000);\n    }\n\n    private GetDisplayTime(): string {\n        let now = Date.now();\n        let nowAsDate = new Date();\n        let diff = now - this.utcTime;\n        let yesterdayMs = now - 86400000;\n        let yesterdayTemp = new Date(yesterdayMs);\n        let yesterday = new Date(yesterdayTemp.getFullYear(), yesterdayTemp.getMonth(), yesterdayTemp.getDate());\n        const dateOptions: Intl.DateTimeFormatOptions = { weekday: \"long\", year: \"numeric\", month: \"long\", day: \"numeric\" };\n        const timeOptions: Intl.DateTimeFormatOptions = { hour: \"numeric\", minute: \"2-digit\" };\n        if (diff > 3599000) {\n          // more than an hour\n            if (this.utcTimeAsDate.toLocaleDateString() === nowAsDate.toLocaleDateString())\n            return PopForums.localizations.todayTime.replace(\"{0}\", this.utcTimeAsDate.toLocaleTimeString(undefined, timeOptions));\n            if (this.utcTimeAsDate.toLocaleDateString() === yesterday.toLocaleDateString())\n                return PopForums.localizations.yesterdayTime.replace(\"{0}\", this.utcTimeAsDate.toLocaleTimeString(undefined, timeOptions));\n            return this.utcTimeAsDate.toLocaleDateString(undefined, dateOptions) + \" \" + this.utcTimeAsDate.toLocaleTimeString(undefined, timeOptions);\n        }\n        if (diff > 120000)\n            return  PopForums.localizations.minutesAgo.replace(\"{0}\", Math.round(diff / 60000).toString());\n        if (diff > 60000)\n            return PopForums.localizations.oneMinuteAgo;\n        return PopForums.localizations.lessThanMinute;\n    }\n\n    static get observedAttributes() { return [\"utctime\"]; }\n\n    attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n        if (name === \"utctime\" && this.isReady) {\n            this.setBaseTime();\n            this.innerHTML = this.GetDisplayTime();\n            this.UpdateTimer();\n        }\n      }\n}\n\ncustomElements.define('pf-formattedtime', FormattedTime);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/FullText.ts",
    "content": "namespace PopForums {\n\n    export class FullText extends ElementBase<TopicState> {\n    constructor() {\n        super();\n    }\n\n    get overridelistener(): string {\n        return this.getAttribute(\"overridelistener\")!;\n    }\n\n    get forcenoimage(): string {\n        return this.getAttribute(\"forcenoimage\")!;\n    }\n\n    get isshort(): string {\n        return this.getAttribute(\"isshort\")!;\n    }\n\n    get formID() { return this.getAttribute(\"formid\") };\n\n    get value() { return this._value;}\n    set value(v: string) { this._value = v; }\n\n    _value!: string;\n\n    static formAssociated = true;\n\n    private textBox!: HTMLElement;\n    private externalFormElement!: HTMLElement;\n\n    connectedCallback() {\n        var initialValue = this.getAttribute(\"value\");\n        if (initialValue)\n            this.value = initialValue;\n        if (userState.isPlainText) {\n            this.externalFormElement = document.createElement(\"textarea\");\n            this.externalFormElement.id = this.formID!;\n            this.externalFormElement.setAttribute(\"name\", this.formID!);\n            this.externalFormElement.classList.add(\"form-control\");\n            if (this.value)\n            (this.externalFormElement as HTMLTextAreaElement).value = this.value;\n            (this.externalFormElement as HTMLTextAreaElement).rows = 12;\n            if (this.isshort?.toLowerCase() === \"true\")\n                (this.externalFormElement as HTMLTextAreaElement).rows = 3;\n            let self = this;\n            this.externalFormElement.addEventListener(\"change\", () => {\n                self.value = (this.externalFormElement as HTMLTextAreaElement).value;\n            });\n            this.appendChild(this.externalFormElement);\n            if (this.overridelistener?.toLowerCase() !== \"true\")\n                super.connectedCallback();\n            return;\n        }\n        let template = document.createElement(\"template\");\n        template.innerHTML = FullText.template;\n        this.attachShadow({ mode: \"open\" });\n        this.shadowRoot!.append(template.content.cloneNode(true));\n        this.textBox = this.shadowRoot!.querySelector(\"#editor\")!;\n        if (this.value)\n            (this.textBox as HTMLTextAreaElement).innerText = this.value;\n        this.editorSettings.target = this.textBox;\n        if (!userState.isImageEnabled || this.forcenoimage?.toLowerCase() === \"true\")\n            this.editorSettings.toolbar = FullText.postNoImageToolbar;\n        if (this.isshort?.toLowerCase() === \"true\")\n            this.editorSettings.height = \"200\";\n        var self = this;\n        this.editorSettings.setup = function (editor: any) {\n            editor.on(\"init\", function (this: any) {\n              this.on(\"focusout\", function(e: any) {\n                editor.save();\n                self.value = (self.textBox as HTMLInputElement).value;\n                (self.externalFormElement as any).value = self.value;\n              });\n              this.on(\"blur\", function(e: any) {\n                editor.save();\n                self.value = (self.textBox as HTMLInputElement).value;\n                (self.externalFormElement as any).value = self.value;\n              });\n              editor.save();\n              self.value = (self.textBox as HTMLInputElement).value;\n              (self.externalFormElement as any).value = self.value;\n            });\n\n            function InstantImageUpload() {\n                const input = document.createElement(\"input\");\n                input.setAttribute(\"type\", \"file\");\n                input.setAttribute(\"accept\", \"image/jpeg,image/gif,image/png\");\n                input.addEventListener(\"change\", (e) => {\n                    const file = input.files![0];\n                    let url = \"/Forums/Image/UploadPostImage\";\n                    let form = new FormData();\n                    form.append(\"file\", file);\n                    editor.setProgressState(true);\n                    fetch(url, {\n                        method: \"POST\",\n                        body: form\n                    })\n                        .then(response => {\n                            if (response.ok)\n                                return response.json();\n                            alert(\"Could not upload image: \" + response.status);\n                        })\n                        .then(payload => {\n                            editor.insertContent(`<img src=\"${payload.url}\" />`);\n                            PopForums.userState.postImageIds.push(payload.id);\n                        })\n                        .catch(error => {\n                            alert(\"Could not upload image: \" + error);\n                        })\n                        .finally(() => {\n                            editor.setProgressState(false);\n                            input.remove();\n                        });\n                });\n                input.style.display = \"none\";\n                document.getElementById(\"ForumContainer\")!.append(input);\n                input.click();\n            };\n\n            editor.ui.registry.addButton(\"imageup\", {\n                icon: \"upload\",\n                tooltip: \"Upload Image\",\n                onAction: () => InstantImageUpload()\n            });\n\n        };\n        tinymce.init(this.editorSettings);\n        this.externalFormElement = document.createElement(\"input\") as HTMLInputElement;\n        this.externalFormElement.id = this.formID!;\n        this.externalFormElement.setAttribute(\"name\", this.formID!);\n        (this.externalFormElement as HTMLInputElement).type = \"hidden\";\n        this.appendChild(this.externalFormElement);\n        if (this.overridelistener?.toLowerCase() !== \"true\")\n            super.connectedCallback();\n    }\n\n    getDependentReference(): [TopicState, string] {\n        return [PopForums.currentTopicState, \"nextQuote\"];\n    }\n\n    updateUI(data: any): void {\n        if (data !== null && data !== undefined)\n        {\n            if (userState.isPlainText) {\n                (this.externalFormElement as HTMLTextAreaElement).value += data;\n                this.value = (this.externalFormElement as HTMLTextAreaElement).value;\n            }\n            else {\n                let editor = tinymce.get(\"editor\");\n                var content = editor.getContent();\n                content += data;\n                editor.setContent(content);\n                (this.textBox as HTMLInputElement).value += content;\n                editor.save();\n                this.value = (this.textBox as HTMLInputElement).value;\n                (this.externalFormElement as HTMLInputElement).value = this.value;\n            }\n        }\n    }\n\n        private static editorCSS = \"/PopForums/lib/bootstrap/dist/css/bootstrap.min.css,/PopForums/lib/PopForums/dist/Editor.min.css\";\n    private static postNoImageToolbar = \"bold italic | bullist numlist blockquote removeformat | link\";\n    editorSettings = {\n        license_key: 'gpl',\n        target: null as unknown as HTMLElement,\n        plugins: \"lists image link\",\n        content_css: FullText.editorCSS,\n        menubar: false,\n        toolbar: \"bold italic | bullist numlist blockquote removeformat | link | image imageup\",\n        statusbar: false,\n        link_target_list: false,\n        link_title: false,\n        image_description: false,\n        image_dimensions: false,\n        image_title: false,\n        image_uploadtab: false,\n        images_file_types: 'jpeg,jpg,png,gif',\n        automatic_uploads: false,\n        browser_spellcheck : true,\n        object_resizing: false,\n        relative_urls: false,\n        remove_script_host: false,\n        contextmenu: \"\",\n        paste_as_text: true,\n        paste_data_images: false,\n        setup: null as unknown as Function,\n        height: null as unknown as string\n    };\n\n    static id: string = \"FullText\";\n    static template: string = `<textarea id=\"editor\"></textarea>\n    `;\n}\n\ncustomElements.define('pf-fulltext', FullText);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/HomeUpdater.ts",
    "content": "namespace PopForums {\n\n    export class HomeUpdater extends HTMLElement {\n        constructor() {\n            super();\n        }\n\n        async connectedCallback() {\n            let service = await MessagingService.GetService();\n            let connection = service.connection;\n            let self = this;\n            connection.on(\"notifyForumUpdate\", function (data: any) {\n                self.updateForumStats(data);\n            });\n            await connection.invoke(\"listenToAllForums\");\n        }\n\n        updateForumStats(data: any) {\n            let row = document.querySelector(\"[data-forumid='\" + data.forumID + \"']\")!;\n            row.querySelector(\".topicCount\")!.innerHTML = data.topicCount;\n            row.querySelector(\".postCount\")!.innerHTML = data.postCount;\n            row.querySelector(\".lastPostName\")!.innerHTML = data.lastPostName;\n            row.querySelector(\"pf-formattedtime\")!.setAttribute(\"utctime\", data.utc);\n            row.querySelector(\".newIndicator .icon\")!.classList.remove(\"text-muted\", \"icon-file-earmark-text\");\n            row.querySelector(\".newIndicator .icon\")!.classList.add(\"text-warning\", \"icon-file-earmark-text-fill\");\n        };\n    }\n\n    customElements.define('pf-homeupdater', HomeUpdater);\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/LoginForm.ts",
    "content": "namespace PopForums {\n\n    export class LoginForm extends HTMLElement {\n        constructor() {\n            super();\n        }\n\n        get templateid() {\n            return this.getAttribute(\"templateid\");\n        }\n        get isExternalLogin() {\n            return this.getAttribute(\"isexternallogin\");\n        }\n\n        private button!: HTMLInputElement;\n        private email!: HTMLInputElement;\n        private password!: HTMLInputElement;\n\n        connectedCallback() {\n            let template = document.getElementById(this.templateid!) as HTMLTemplateElement;\n            if (!template) {\n                console.error(`Can't find templateID ${this.templateid} to make login form.`);\n                return;\n            }\n            this.append(template.content.cloneNode(true));\n            this.email = this.querySelector(\"#EmailLogin\")!;\n            this.password = this.querySelector(\"#PasswordLogin\")!;\n            this.button = this.querySelector(\"#LoginButton\")!;\n            this.button.addEventListener(\"click\", () => {\n                this.executeLogin();\n            });\n\t\t\tthis.querySelectorAll(\"#EmailLogin,#PasswordLogin\").forEach(x =>\n\t\t\t\tx.addEventListener(\"keydown\", (e) => {\n\t\t\t\t\tif ((e as KeyboardEvent).code === \"Enter\") this.executeLogin();\n\t\t\t\t})\n            );\n        }\n\n        executeLogin() {\n            let path = \"/Identity/Login\";\n            if (this.isExternalLogin!.toLowerCase() === \"true\")\n                path = \"/Identity/LoginAndAssociate\";\n            let payload = JSON.stringify({ email: this.email.value, password: this.password.value });\n            fetch(PopForums.AreaPath + path, {\n                method: \"POST\",\n                headers: {\n                    'Content-Type': 'application/json'\n                },\n                body: payload\n            })\n                .then(function(response) {\n                    return response.json();\n            })\n                .then(function (result) {\n                    switch (result.result) {\n                    case true:\n                        let destination = (document.querySelector(\"#Referrer\") as HTMLInputElement).value;\n                        location.href = destination;\n                        break;\n                    default:\n                        let loginResult = document.querySelector(\"#LoginResult\")!;\n                        loginResult.innerHTML = result.message;\n                        loginResult.classList.remove(\"d-none\");\n                    }\n            })\n                .catch(function (error) {\n                    let loginResult = document.querySelector(\"#LoginResult\")!;\n                    loginResult.innerHTML = \"There was an unknown error while attempting login\";\n                    loginResult.classList.remove(\"d-none\");\n            });\n        }\n    }\n\n    customElements.define('pf-loginform', LoginForm);\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/MorePostsBeforeReplyButton.ts",
    "content": "namespace PopForums {\n\n    export class MorePostsBeforeReplyButton extends ElementBase<TopicState> {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = MorePostsBeforeReplyButton.template;\n        let button = this.querySelector(\"input\") as HTMLInputElement;\n        button.value = this.buttontext;\n        if (this.buttonclass?.length > 0)\n            this.buttonclass.split(\" \").forEach((c) => button.classList.add(c));\n        button.addEventListener(\"click\", (e: MouseEvent) => {\n            PopForums.currentTopicState.loadMorePosts();\n        });\n        super.connectedCallback();\n    }\n\n    getDependentReference(): [TopicState, string] {\n        return [PopForums.currentTopicState, \"isNewerPostsAvailable\"];\n    }\n\n    updateUI(data: boolean): void {\n        let button = this.querySelector(\"input\")!;\n        if (!data)\n            button.style.visibility = \"hidden\";\n        else\n            button.style.visibility = \"visible\";\n    }\n\n    static template: string = `<input type=\"button\" />`;\n}\n\ncustomElements.define('pf-morepostsbeforereplybutton', MorePostsBeforeReplyButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/MorePostsButton.ts",
    "content": "namespace PopForums {\n\n    export class MorePostsButton extends ElementBase<TopicState> {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = MorePostsButton.template;\n        let button = this.querySelector(\"input\") as HTMLInputElement;\n        button.value = this.buttontext;\n        if (this.buttonclass?.length > 0)\n            this.buttonclass.split(\" \").forEach((c) => button.classList.add(c));\n        button.addEventListener(\"click\", (e: MouseEvent) => {\n            PopForums.currentTopicState.loadMorePosts();\n        });\n        super.connectedCallback();\n    }\n\n    getDependentReference(): [TopicState, string] {\n        return [PopForums.currentTopicState, \"highPage\"];\n    }\n\n    updateUI(data: number): void {\n        let button = this.querySelector(\"input\")!;\n        if (PopForums.currentTopicState.pageCount === 1 || data === PopForums.currentTopicState.pageCount)\n            button.style.visibility = \"hidden\";\n        else\n            button.style.visibility = \"visible\";\n    }\n\n    static template: string = `<input type=\"button\" />`;\n}\n\ncustomElements.define('pf-morepostsbutton', MorePostsButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/NotificationItem.ts",
    "content": "namespace PopForums {\n\n    export class NotificationItem extends HTMLElement {\n    constructor(notification: Notification) {\n        super();\n        this.notification = notification;\n    }\n\n    notification: Notification;\n\n    private escapeHtml(s: string): string {\n        return (s ?? \"\").replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n    }\n\n    connectedCallback() {\n        let markup: string = \"\";\n        let link: string = \"\";\n        switch (this.notification.notificationType) {\n            case 3: // Award\n                markup = `${PopForums.localizations.award}: <b>${this.escapeHtml(this.notification.data.title)}</b>`;\n                link = \"/Forums/Account/ViewProfile/\" + this.notification.userID + \"#Awards\";\n                break;\n            case 2: // QuestionAnswered\n                markup = PopForums.localizations.questionAnsweredNotification\n                    .replace(\"{0}\", this.escapeHtml(this.notification.data.askerName))\n                    .replace(\"{1}\", this.escapeHtml(this.notification.data.title));\n                link = \"/Forums/PostLink/\" + this.notification.data.postID;\n                break;\n            case 0: // NewReply\n                markup = PopForums.localizations.newReplyNotification\n                    .replace(\"{0}\", this.escapeHtml(this.notification.data.postName))\n                    .replace(\"{1}\", this.escapeHtml(this.notification.data.title));\n                link = \"/Forums/Forum/GoToNewestPost/\" + this.notification.data.topicID;\n                break;\n            case 1: // VoteUp\n                markup = PopForums.localizations.voteUpNotification\n                    .replace(\"{0}\", this.escapeHtml(this.notification.data.voterName))\n                    .replace(\"{1}\", this.escapeHtml(this.notification.data.title));\n                link = \"/Forums/PostLink/\" + this.notification.data.postID;\n                break;\n            default:\n                console.log(`Unknown notification type: ${this.notification.notificationType}`);\n        }\n        let newness = \" border border-2\";\n        if (!this.notification.isRead)\n            newness = \" text-bg-light border-primary border border-2\";\n        let template = `<div class=\"card mb-3${newness}\">\n    <div class=\"card-body\">\n        <p>${markup}</p>\n    </div>\n    <div class=\"card-footer text-end\">\n    </div>\n    <a href=\"${link}\" class=\"stretched-link\"></a>\n</div>`;\n        this.innerHTML = template;\n        let timeStamp = new FormattedTime();\n        timeStamp.setAttribute(\"utctime\", this.notification.timeStamp.toString());\n        let footer = this.querySelector(\".card-footer\")!;\n        footer.append(timeStamp);\n\n        this.querySelector(\"a\")!.addEventListener(\"click\", (e) => {\n            PopForums.userState.MarkRead(this.notification.contextID, this.notification.notificationType);\n        });\n    }\n\n    MarkRead() {\n        let box = this.querySelector(\".card\");\n        if (box) {\n            box.classList.remove(\"text-bg-light\", \"border-primary\");\n        }\n        this.notification.isRead = true;\n    }\n\n}\n\ncustomElements.define('pf-notificationitem', NotificationItem);\n\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/NotificationList.ts",
    "content": "namespace PopForums {\n\n    export class NotificationList extends ElementBase<UserState> {\n    constructor() {\n        super();\n    }\n\n    connectedCallback() {\n        super.connectedCallback();\n    }\n\n    getDependentReference(): [UserState, string] {\n        return [PopForums.userState, \"notifications\"];\n    }\n\n    updateUI(data: Array<Notification>): void {\n        if (!data || data.length === 0) {\n            this.replaceChildren();\n            return;\n        }\n        data.forEach(item => {\n            let n = new NotificationItem(item);\n            this.append(n);\n        });\n    }\n}\n\ncustomElements.define('pf-notificationlist', NotificationList);\n\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/NotificationMarkAllButton.ts",
    "content": "namespace PopForums {\n\n    export class NotificationMarkAllButton extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = `<input type=\"button\" class=\"${this.buttonclass}\" value=\"${this.buttontext}\" data-bs-dismiss=\"offcanvas\" />`;\n        this.querySelector(\"input\")!.addEventListener(\"click\", () => {\n            PopForums.userState.MarkAllRead();\n        });\n    }\n}\n\ncustomElements.define('pf-notificationmarkallbutton', NotificationMarkAllButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/NotificationToggle.ts",
    "content": "namespace PopForums {\n\n    export class NotificationToggle extends ElementBase<UserState> {\n    constructor() {\n        super();\n        this.userState = PopForums.userState;\n        this.isInit = false;\n    }\n\n    get panelid(): string {\n        return this.getAttribute(\"panelid\")!;\n    }\n\n    get notificationlistid(): string {\n        return this.getAttribute(\"notificationlistid\")!;\n    }\n\n    private userState: UserState;\n    private isReady!: boolean;\n    private panel!: HTMLElement;\n    private offCanvas!: bootstrap.Offcanvas;\n    private isInit: boolean;\n\n    connectedCallback() {\n        const delegate = this.ready.bind(this);\n        this.isReady = LocalizationService.subscribe(delegate);\n        if (this.isReady)\n            this.ready();\n        super.connectedCallback();\n    }\n\n    ready() {\n        this.title = PopForums.localizations.notifications;\n        this.panel = document.getElementById(this.panelid)!;\n        this.offCanvas = new bootstrap.Offcanvas(this.panel);\n        this.addEventListener(\"click\", this.show);\n        let list = document.getElementById(this.notificationlistid)!;\n        this.userState.list = list;\n    }\n\n    private async show() {\n        this.offCanvas.show();\n        this.panel.addEventListener(\"hide.bs.offcanvas\", event => {\n            this.userState.list.removeEventListener(\"scroll\", this.userState.ScrollLoad);\n        });\n        await this.userState.LoadNotifications();\n        this.userState.list.addEventListener(\"scroll\", this.userState.ScrollLoad);\n    }\n\n    getDependentReference(): [UserState, string] {\n        return [PopForums.userState, \"notificationCount\"];\n    }\n\n    updateUI(data: number): void {\n        if (data === 0)\n            this.innerHTML = `<span class=\"icon icon-bell-fill\"></span>`;\n        else {\n            this.innerHTML = `<span class=\"icon icon-bell-fill\"></span><span class=\"badge ms-1\">${data}</span>`;\n            if (this.isInit) {\n                this.innerHTML = `<span class=\"icon icon-bell-fill\"></span><span class=\"badge ms-1 explode\">${data}</span>`;\n            }\n        }\n        this.isInit = true;\n    }\n}\n\ncustomElements.define('pf-notificationtoggle', NotificationToggle);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/PMCount.ts",
    "content": "﻿namespace PopForums {\n\nexport class PMCount extends ElementBase<UserState> {\n    constructor() {\n        super();\n        this.isInit = false;\n    }\n    \n    private isInit: boolean;\n\n    getDependentReference(): [UserState, string] {\n        return [PopForums.userState, \"newPmCount\"];\n    }\n\n    updateUI(data: number): void {\n        if (data === 0)\n            this.innerHTML = \"\";\n        else {\n            this.innerHTML = `<span class=\"badge\">${data}</span>`;\n            if (this.isInit)\n                this.innerHTML = `<span class=\"badge explode\">${data}</span>`;\n        }\n        this.isInit = true;\n    }\n}\n\ncustomElements.define('pf-pmcount', PMCount);\n\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/PMForm.ts",
    "content": "namespace PopForums {\n\nexport class PMForm extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    private isReady!: boolean;\n\n    connectedCallback() {\n        const delegate = this.ready.bind(this);\n        this.isReady = LocalizationService.subscribe(delegate);\n        if (this.isReady)\n            this.ready();\n    }\n\n    ready() {\n        this.innerHTML = PMForm.template;\n        let button = this.querySelector(\"button\")!;\n        button.innerHTML = PopForums.localizations.send;\n        let textBox = this.querySelector(\"textarea\") as HTMLTextAreaElement;\n        textBox.addEventListener(\"keydown\", (e) => {\n            if (e.key === \"Enter\" && !e.shiftKey) {\n                e.preventDefault();\n                this.send(textBox);\n            }\n        });\n        button.addEventListener(\"click\", () => {\n            this.send(textBox);\n        });\n    }\n\n    private send(textBox: HTMLTextAreaElement) {\n        PopForums.currentPmState.send(textBox.value);\n        textBox.value = \"\";\n    }\n\n    static template: string =\n`<div class=\"input-group mb-3\">\n    <textarea class=\"form-control\"></textarea>\n    <button class=\"btn btn-primary\" type=\"button\"></button>\n</div>`;\n}\n\ncustomElements.define('pf-pmform', PMForm);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/PostMiniProfile.ts",
    "content": "namespace PopForums {\n\n    export class PostMiniProfile extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    get username(): string {\n        return this.getAttribute(\"username\")!;\n    }\n    get usernameclass(): string {\n        return this.getAttribute(\"usernameclass\")!;\n    }\n    get userid(): string {\n        return this.getAttribute(\"userid\")!;\n    }\n    get miniProfileBoxClass(): string {\n        return this.getAttribute(\"miniprofileboxclass\")!;\n    }\n\n    private isOpen!: boolean;\n    private box!: HTMLElement;\n    private boxHeight!: string;\n    private isLoaded!: boolean;\n\n    connectedCallback() {\n        this.isLoaded = false;\n        this.innerHTML = PostMiniProfile.template;\n        let nameHeader = this.querySelector(\"h3\") as HTMLElement;\n        this.usernameclass.split(\" \").forEach((c) => nameHeader.classList.add(c));\n        nameHeader.innerHTML = this.username;\n        nameHeader.addEventListener(\"click\", () => {\n            this.toggle();\n        });\n        this.box = this.querySelector(\"div\")!;\n        this.miniProfileBoxClass.split(\" \").forEach((c) => this.box.classList.add(c));\n    }\n\n    private toggle() {\n        if (!this.isLoaded) {\n            fetch(PopForums.AreaPath + \"/Account/MiniProfile/\" + this.userid)\n                .then(response => response.text()\n                    .then(text => {\n                        let sub = this.box.querySelector(\"div\")!;\n                        sub.innerHTML = text;\n                        const height = sub.getBoundingClientRect().height;\n                        this.boxHeight = `${height}px`;\n                        this.box.style.height = this.boxHeight;\n                        this.isOpen = true;\n                        this.isLoaded = true;\n                    }));\n        }\n        else if (!this.isOpen) {\n            this.box.style.height = this.boxHeight;\n            this.isOpen = true;\n        }\n        else {\n            this.box.style.height = \"0\";\n            this.isOpen = false;\n        }\n    }\n\n    static template: string = `<h3></h3>\n<div>\n    <div class=\"py-1 px-3 mb-2\"></div>\n</div>`;\n}\n\ncustomElements.define('pf-postminiprofile', PostMiniProfile);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/PostModerationLogButton.ts",
    "content": "namespace PopForums {\n\n    export class PostModerationLogButton extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n\n    get postid(): string {\n        return this.getAttribute(\"postid\")!;\n    }\n\n    get parentSelectorToAppendTo(): string {\n        return this.getAttribute(\"parentselectortoappendto\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = PostModerationLogButton.template;\n        let button = this.querySelector(\"input\")!;\n        button.value = this.buttontext;\n        let classes = this.buttonclass;\n        if (classes?.length > 0)\n            classes.split(\" \").forEach((c) => button.classList.add(c));\n        let self = this;\n        let container: HTMLDivElement;\n        button.addEventListener(\"click\", () => {\n            if (!container) {\n                let parentContainer = self.closest(this.parentSelectorToAppendTo);\n                if (!parentContainer) {\n                    console.error(`Can't find a parent selector \"${this.parentSelectorToAppendTo}\" to append post moderation log to.`);\n                    return;\n                }\n                container = document.createElement(\"div\");\n                container.classList.add(\"my-3\");\n                parentContainer.appendChild(container);\n            }\n            if (container.style.display !== \"block\")\n                fetch(PopForums.AreaPath + \"/Moderator/PostModerationLog/\" + this.postid)\n                    .then(response => response.text()\n                        .then(text => {\n                            container.innerHTML = text;\n                            container.style.display = \"block\";\n                        }));\n            else container.style.display = \"none\";\n        });\n    }\n\n    static template: string = `<input type=\"button\" />`;\n}\n\ncustomElements.define(\"pf-postmoderationlogbutton\", PostModerationLogButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/PreviewButton.ts",
    "content": "namespace PopForums {\n\n    export class PreviewButton extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    get labelText(): string {\n        return this.getAttribute(\"labeltext\")!;\n    }\n    get textSourceSelector(): string {\n        return this.getAttribute(\"textsourceselector\")!;\n    }\n    get isPlainTextSelector(): string {\n        return this.getAttribute(\"isplaintextselector\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = PreviewButton.template;\n        let button = this.querySelector(\"input\")!;\n        button.value = this.labelText;\n        let headText = this.querySelector(\"h4\") as HTMLHeadElement;\n        headText.innerText = this.labelText;\n        var modal = this.querySelector(\".modal\")!;\n        modal.addEventListener(\"shown.bs.modal\", () => {\n            this.openModal();\n        });\n    }\n\n    openModal() {\n        tinymce.triggerSave();\n        let fullText = document.querySelector(this.textSourceSelector) as any;\n        let model = {\n            FullText: fullText.value,\n            IsPlainText: (document.querySelector(this.isPlainTextSelector) as HTMLInputElement).value.toLowerCase() === \"true\"\n        };\n        fetch(PopForums.AreaPath + \"/Forum/PreviewText\", {\n            method: \"POST\",\n            body: JSON.stringify(model),\n            headers: {\n                \"Content-Type\": \"application/json\"\n            }\n        })\n            .then(response => response.text()\n                .then(text => {\n                    let r = this.querySelector(\".parsedFullText\") as HTMLDivElement;\n                    r.innerHTML = text;\n                }));\n    }\n\n    static template: string = `<input type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#PreviewModal\">\n<div class=\"modal fade\" id=\"PreviewModal\" tabindex=\"-1\" role=\"dialog\">\n\t<div class=\"modal-dialog\">\n\t\t<div class=\"modal-content\">\n\t\t\t<div class=\"modal-header\">\n\t\t\t\t<h4 class=\"modal-title\"></h4>\n\t\t\t\t<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n\t\t\t</div>\n\t\t\t<div class=\"modal-body\">\n\t\t\t\t<div class=\"postItem parsedFullText\"></div>\n\t\t\t</div>\n\t\t\t<div class=\"modal-footer\">\n\t\t\t\t<button type=\"button\" class=\"btn btn-primary\" data-bs-dismiss=\"modal\">Close</button>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>`;\n}\n\ncustomElements.define('pf-previewbutton', PreviewButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/PreviousPostsButton.ts",
    "content": "namespace PopForums {\n\n    export class PreviousPostsButton extends ElementBase<TopicState> {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = PreviousPostsButton.template;\n        let button = this.querySelector(\"input\") as HTMLInputElement;\n        button.value = this.buttontext;\n        if (this.buttonclass?.length > 0)\n            this.buttonclass.split(\" \").forEach((c) => button.classList.add(c));\n        button.addEventListener(\"click\", (e: MouseEvent) => {\n            PopForums.currentTopicState.loadPreviousPosts();\n        });\n        super.connectedCallback();\n    }\n\n    getDependentReference(): [TopicState, string] {\n        return [PopForums.currentTopicState, \"lowPage\"];\n    }\n\n    updateUI(data: number): void {\n        let button = this.querySelector(\"input\")!;\n        if (PopForums.currentTopicState.pageCount === 1 || data === 1)\n            button.style.visibility = \"hidden\";\n        else\n            button.style.visibility = \"visible\";\n    }\n\n    static template: string = `<input type=\"button\" />`;\n}\n\ncustomElements.define('pf-previouspostsbutton', PreviousPostsButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/QuoteButton.ts",
    "content": "namespace PopForums {\n\n    export class QuoteButton extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    get name(): string {\n        return this.getAttribute(\"name\")!;\n    }\n    get containerid(): string {\n        return this.getAttribute(\"containerid\")!;\n    }\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n    get tip(): string {\n        return this.getAttribute(\"tip\")!;\n    }\n    get postID(): string {\n        return this.getAttribute(\"postid\")!;\n    }\n\n    private _tip: any;\n\n    connectedCallback() {\n        let targetText = document.getElementById(this.containerid)!;\n        this.innerHTML = QuoteButton.template;\n        let button = this.querySelector(\"button\")!;\n        button.title = this.tip;\n        [\"mousedown\",\"touchstart\"].forEach((e:string) =>\n            targetText.addEventListener(e, () =>\n                { if (this._tip) this._tip.hide() }));\n        button.value = this.buttontext;\n        let classes = this.buttonclass;\n        if (classes?.length > 0)\n            classes.split(\" \").forEach((c) => button.classList.add(c));\n        this.onclick = (e: MouseEvent) => {\n            // get this from topic state's callback/ready method, because iOS loses selection when you touch quote button\n            let fragment = PopForums.currentTopicState.documentFragment;\n            let ancestor = PopForums.currentTopicState.selectionAncestor as Element;\n            if (!fragment) {\n                let selection = document.getSelection();\n                if (!selection || selection.rangeCount === 0 || selection.getRangeAt(0).toString().length === 0) {\n                    // prompt to select\n                    this._tip = new bootstrap.Tooltip(button, {trigger: \"manual\"});\n                    this._tip.show();\n                    return;\n                }\n                let range = selection.getRangeAt(0);\n                ancestor = range.commonAncestorContainer as Element;\n                fragment = range.cloneContents();\n            }\n            let div = document.createElement(\"div\");\n            div.appendChild(fragment);\n            // is selection in the container?\n            while (ancestor.id !== this.containerid && ancestor.parentElement !== null) {\n                ancestor = ancestor.parentElement;\n            }\n            let isInText = ancestor.id === this.containerid;\n            // if not, is it partially in the container?\n            if (!isInText) {\n                let container = div.querySelector(\"#\" + this.containerid);\n                if (container !== null && container !== undefined) {\n                    // it's partially in the container, so just get that part\n                    div.innerHTML = container.innerHTML;\n                    isInText = true;\n                }\n            }\n            if (isInText) {\n                // activate or add to quote\n                let result: string;\n                if (userState.isPlainText)\n                    result = `[quote][i]${this.name}:[/i]\\r\\n ${div.innerText}[/quote]`;\n                else\n                    result = `<blockquote><p><i>${this.name}:</i></p>${div.innerHTML}</blockquote><p></p>`;\n                PopForums.currentTopicState.nextQuote = result;\n                if (!PopForums.currentTopicState.isReplyLoaded)\n                    PopForums.currentTopicState.loadReply(PopForums.currentTopicState.topicID, Number(this.postID), true);\n            }\n            let temp = document.getSelection();\n            if (temp)\n                temp.removeAllRanges();\n        };\n    }\n\n    static template: string = `<button type=\"button\" data-bs-toggle=\"tooltip\" title=\"\" />`;\n}\n\ncustomElements.define('pf-quotebutton', QuoteButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/ReplyButton.ts",
    "content": "namespace PopForums {\n\n    export class ReplyButton extends ElementBase<TopicState> {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n\n    get topicid(): string {\n        return this.getAttribute(\"topicid\")!;\n    }\n\n    get postid(): string {\n        return this.getAttribute(\"postid\")!;\n    }\n\n    get overridedisplay(): string {\n        return this.getAttribute(\"overridedisplay\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = ReplyButton.template;\n        let button = this.querySelector(\"button\") as HTMLButtonElement;\n        button.title = this.buttontext;\n        if (this.buttonclass?.length > 0)\n            this.buttonclass.split(\" \").forEach((c) => button.classList.add(c));\n        if (button.classList.contains(\"btn\"))\n            button.innerText = this.buttontext;\n        button.addEventListener(\"click\", (e: MouseEvent) => {\n            PopForums.currentTopicState.loadReply(Number(this.topicid), Number(this.postid), true);\n        });\n        super.connectedCallback();\n    }\n\n    getDependentReference(): [TopicState, string] {\n        return [PopForums.currentTopicState, \"isReplyLoaded\"];\n    }\n\n    updateUI(data: boolean): void {\n        if (this.overridedisplay?.toLowerCase() === \"true\")\n            return;\n        let button = this.querySelector(\":first-child\") as HTMLElement;\n        if (data)\n            button.style.display = \"none\";\n        else\n            button.style.display = \"initial\";\n    }\n\n    static template: string = `<button type=\"button\"></button>`;\n}\n\ncustomElements.define('pf-replybutton', ReplyButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/ReplyForm.ts",
    "content": "namespace PopForums {\n\n    export class ReplyForm extends HTMLElement {\n        constructor() {\n            super();\n        }\n\n        get templateID() {\n            return this.getAttribute(\"templateid\");\n        }\n\n        private button!: HTMLInputElement;\n\n        connectedCallback() {\n            let template = document.getElementById(this.templateID!) as HTMLTemplateElement;\n            if (!template) {\n                console.error(`Can't find templateID ${this.templateID} to make reply form.`);\n                return;\n            }\n            this.append(template.content.cloneNode(true));\n            this.button = this.querySelector(\"#SubmitReply\")!;\n            this.button.addEventListener(\"click\", () => {\n                this.submitReply();\n            });\n        }\n\n        submitReply() {\n            this.button.setAttribute(\"disabled\", \"disabled\");\n            let closeCheck = document.querySelector(\"#CloseOnReply\") as HTMLInputElement;\n            let closeOnReply = false;\n            if (closeCheck && closeCheck.checked) closeOnReply = true;\n            let postImageIDs = PopForums.userState.postImageIds;\n            let model = {\n                Title: (this.querySelector(\"#NewReply #Title\") as HTMLInputElement).value,\n                FullText: (this.querySelector(\"#NewReply #FullText\") as HTMLInputElement).value,\n                IncludeSignature: (this.querySelector(\"#NewReply #IncludeSignature\") as HTMLInputElement).checked,\n                ItemID: (this.querySelector(\"#NewReply #ItemID\") as HTMLInputElement).value,\n                CloseOnReply: closeOnReply,\n                IsPlainText: (this.querySelector(\"#NewReply #IsPlainText\") as HTMLInputElement).value.toLowerCase() === \"true\",\n                ParentPostID: (this.querySelector(\"#NewReply #ParentPostID\") as HTMLInputElement).value,\n                PostImageIDs: postImageIDs\n            };\n            fetch(PopForums.AreaPath + \"/Forum/PostReply\", {\n                method: \"POST\",\n                body: JSON.stringify(model),\n                headers: {\n                    \"Content-Type\": \"application/json\"\n                },\n            })\n                .then(response => response.json())\n                .then(result => {\n                    switch (result.result) {\n                        case true:\n                            window.location = result.redirect;\n                            break;\n                        default:\n                            var r = this.querySelector(\"#PostResponseMessage\") as HTMLElement;\n                            r.innerHTML = result.message;\n                            this.button.removeAttribute(\"disabled\");\n                            r.style.display = \"block\";\n                    }\n                })\n                .catch(error => {\n                    var r = this.querySelector(\"#PostResponseMessage\") as HTMLElement;\n                    r.innerHTML = \"There was an unknown error while trying to post\";\n                    this.button.removeAttribute(\"disabled\");\n                    r.style.display = \"block\";\n                });\n        };\n    }\n\n    customElements.define('pf-replyform', ReplyForm);\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/SearchNavForm.ts",
    "content": "namespace PopForums {\n\n    export class SearchNavForm extends HTMLElement {\n        constructor() {\n            super();\n        }\n\n        get templateid() {\n            return this.getAttribute(\"templateid\");\n        }\n        get textboxid() {\n            return this.getAttribute(\"textboxid\");\n        }\n        get dropdownid() {\n            return this.getAttribute(\"dropdownid\");\n        }\n\n        private searchBox!: HTMLInputElement;\n        private dropdown!: HTMLElement;\n\n        connectedCallback() {\n            let template = document.getElementById(this.templateid!) as HTMLTemplateElement;\n            if (!template) {\n                console.error(`Can't find templateID ${this.templateid} to make search form.`);\n                return;\n            }\n            this.append(template.content.cloneNode(true));\n            this.searchBox = this.querySelector(\"#\" + this.textboxid)!;\n            this.dropdown = this.querySelector(\"#\" + this.dropdownid)!;\n            this.dropdown.addEventListener(\"shown.bs.dropdown\", () => {\n                this.searchBox.focus();\n            });\n        }\n    }\n\n    customElements.define('pf-searchnavform', SearchNavForm);\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/SubscribeButton.ts",
    "content": "namespace PopForums {\n\n    export class SubscribeButton extends ElementBase<TopicState> {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n\n    get subscribetext(): string {\n        return this.getAttribute(\"subscribetext\")!;\n    }\n    get unsubscribetext(): string {\n        return this.getAttribute(\"unsubscribetext\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = SubscribeButton.template;\n        let button: HTMLButtonElement = this.querySelector(\"button\")!;\n        this.buttonclass.split(\" \").forEach((c) => button.classList.add(c));\n        button.addEventListener(\"click\", () => {\n            fetch(PopForums.AreaPath + \"/Subscription/ToggleSubscription/\" + PopForums.currentTopicState.topicID, {\n                method: \"POST\"\n            })\n                .then(response => response.json())\n                .then(result => {\n                    switch (result.data.isSubscribed) {\n                        case true:\n                            PopForums.currentTopicState.isSubscribed = true;\n                            break;\n                        case false:\n                            PopForums.currentTopicState.isSubscribed = false;\n                            break;\n                        default:\n                            // TODO: something else\n                    }\n                })\n                .catch(() => {\n                    // TODO: handle error\n                });\n        });\n        super.connectedCallback();\n    }\n\n    getDependentReference(): [TopicState, string] {\n        return [PopForums.currentTopicState, \"isSubscribed\"];\n    }\n\n    updateUI(data: boolean): void {\n        let button = this.querySelector(\"button\")!;\n        if (data) {\n            button.title = this.unsubscribetext;\n            button.classList.remove(\"icon-bell-slash\", \"text-muted\");\n            button.classList.add(\"icon-bell-fill\", \"text-warning\");\n        }\n        else {\n            button.title = this.subscribetext;\n            button.classList.remove(\"icon-bell-fill\", \"text-warning\");\n            button.classList.add(\"icon-bell-slash\", \"text-muted\");\n        }\n    }\n\n    static template: string = `<button type=\"button\" class=\"btn-link icon\"></button>`;\n}\n\ncustomElements.define('pf-subscribebutton', SubscribeButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/TopicButton.ts",
    "content": "namespace PopForums {\n\n    export class TopicButton extends ElementBase<ForumState> {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n\n    get forumid(): string {\n        return this.getAttribute(\"forumid\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = TopicButton.template;\n        let button = this.querySelector(\"input\") as HTMLInputElement;\n        button.value = this.buttontext;\n        if (this.buttonclass?.length > 0)\n            this.buttonclass.split(\" \").forEach((c) => button.classList.add(c));\n        button.addEventListener(\"click\", () => {\n            currentForumState.loadNewTopic();\n        });\n        super.connectedCallback();\n    }\n\n    getDependentReference(): [ForumState, string] {\n        return [PopForums.currentForumState, \"isNewTopicLoaded\"];\n    }\n\n    updateUI(data: boolean): void {\n        if (data)\n            this.style.display = \"none\";\n        else\n            this.style.display = \"initial\";\n    }\n\n    static template: string = `<input type=\"button\" />`;\n}\n\ncustomElements.define('pf-topicbutton', TopicButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/TopicForm.ts",
    "content": "namespace PopForums {\n\n    export class TopicForm extends HTMLElement {\n        constructor() {\n            super();\n        }\n\n        get templateID() {\n            return this.getAttribute(\"templateid\");\n        }\n\n        private button!: HTMLInputElement;\n\n        connectedCallback() {\n            let template = document.getElementById(this.templateID!) as HTMLTemplateElement;\n            if (!template) {\n                console.error(`Can't find templateID ${this.templateID} to make reply form.`);\n                return;\n            }\n            this.append(template.content.cloneNode(true));\n            this.button = this.querySelector(\"#SubmitNewTopic\")!;\n            this.button.addEventListener(\"click\", () => {\n                this.submitTopic();\n            });\n        }\n\n        submitTopic() {\n            this.button.setAttribute(\"disabled\", \"disabled\");\n            let postImageIDs = PopForums.userState.postImageIds;\n            let model = {\n                Title: (this.querySelector(\"#NewTopic #Title\") as HTMLInputElement).value,\n                FullText: (this.querySelector(\"#NewTopic #FullText\")as HTMLInputElement).value,\n                IncludeSignature: (this.querySelector(\"#NewTopic #IncludeSignature\")as HTMLInputElement).checked,\n                ItemID: (this.querySelector(\"#NewTopic #ItemID\")as HTMLInputElement).value,\n                IsPlainText: (this.querySelector(\"#NewTopic #IsPlainText\")as HTMLInputElement).value.toLowerCase() === \"true\",\n                PostImageIDs: postImageIDs\n            };\n            fetch(PopForums.AreaPath + \"/Forum/PostTopic\", {\n                method: \"POST\",\n                body: JSON.stringify(model),\n                headers: {\n                    \"Content-Type\": \"application/json\"\n                },\n            })\n                .then(response => response.json())\n                .then(result => {\n                    switch (result.result) {\n                        case true:\n                            window.location = result.redirect;\n                            break;\n                        default:\n                            var r = this.querySelector(\"#PostResponseMessage\") as HTMLElement;\n                            r.innerHTML = result.message;\n                            this.button.removeAttribute(\"disabled\");\n                            r.style.display = \"block\";\n                    }\n                })\n                .catch(error => {\n                    var r = this.querySelector(\"#PostResponseMessage\") as HTMLElement;\n                    r.innerHTML = \"There was an unknown error while trying to post\";\n                    this.button.removeAttribute(\"disabled\");\n                    r.style.display = \"block\";\n                });\n        };\n    }\n\n    customElements.define('pf-topicform', TopicForm);\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/TopicModerationLogButton.ts",
    "content": "namespace PopForums {\n\n    export class TopicModerationLogButton extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    get buttonclass(): string {\n        return this.getAttribute(\"buttonclass\")!;\n    }\n\n    get buttontext(): string {\n        return this.getAttribute(\"buttontext\")!;\n    }\n\n    get topicid(): string {\n        return this.getAttribute(\"topicid\")!;\n    }\n\n    connectedCallback() {\n        this.innerHTML = TopicModerationLogButton.template;\n        let button = this.querySelector(\"input\")!;\n        button.value = this.buttontext;\n        let classes = this.buttonclass;\n        if (classes?.length > 0)\n            classes.split(\" \").forEach((c) => button.classList.add(c));\n        button.addEventListener(\"click\", () => {\n            let container = this.querySelector(\"div\")!;\n            if (container.style.display !== \"block\")\n                fetch(PopForums.AreaPath + \"/Moderator/TopicModerationLog/\" + this.topicid)\n                    .then(response => response.text()\n                        .then(text => {\n                            container.innerHTML = text;\n                            container.style.display = \"block\";\n                        }));\n            else container.style.display = \"none\";\n        });\n    }\n\n    static template: string = `<input type=\"button\" class=\"my-2\" />\n    <div></div>`;\n}\n\ncustomElements.define(\"pf-topicmoderationlogbutton\", TopicModerationLogButton);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Components/VoteCount.ts",
    "content": "namespace PopForums {\n\n    export class VoteCount extends HTMLElement {\n    constructor() {\n        super();\n    }\n\n    get votes(): string {\n        return this.getAttribute(\"votes\")!;\n    }\n    set votes(value:string) {\n        this.setAttribute(\"votes\", value);\n    }\n\n    get postid(): string {\n        return this.getAttribute(\"postid\")!;\n    }\n\n    get containerclass(): string {\n        return this.getAttribute(\"containerclass\")!;\n    }\n\n    get votescontainerclass(): string {\n        return this.getAttribute(\"votescontainerclass\")!;\n    }\n\n    get badgeclass(): string {\n        return this.getAttribute(\"badgeclass\")!;\n    }\n\n    get votebuttonclass(): string {\n        return this.getAttribute(\"votebuttonclass\")!;\n    }\n\n    get isloggedin(): string {\n        return this.getAttribute(\"isloggedin\")!.toLowerCase();\n    }\n\n    get isauthor(): string {\n        return this.getAttribute(\"isauthor\")!.toLowerCase();\n    }\n\n    get isvoted(): string {\n        return this.getAttribute(\"isvoted\")!.toLowerCase();\n    }\n\n    private badge!: HTMLElement;\n    private voterContainer!: HTMLElement;\n    private popOver!: bootstrap.Popover;\n    private popoverEventHander!: EventListenerOrEventListenerObject;\n\n    connectedCallback() {\n        this.innerHTML = VoteCount.template;\n        let topContainer = this.querySelector(\"div\")!;\n        if (this.containerclass?.length > 0)\n            this.containerclass.split(\" \").forEach((c) => topContainer.classList.add(c));\n        this.badge = this.querySelector(\"div > div\")!;\n        this.badge.innerHTML = \"+\" + this.votes;\n        if (this.badgeclass?.length > 0)\n            this.badgeclass.split(\" \").forEach((c) => this.badge.classList.add(c));\n        let statusHtml = this.buttonGenerator();\n        if (statusHtml != \"\") {\n            let status = document.createElement(\"template\");\n            status.innerHTML = this.buttonGenerator();\n            this.firstElementChild!.append(status.content.firstChild!);\n        }\n        let voteButton = this.querySelector(\"span\");\n        if (voteButton) {\n            if (this.votebuttonclass?.length > 0)\n                this.votebuttonclass.split(\" \").forEach((c) => voteButton.classList.add(c));\n            type resultType = { votes: number; isVoted: boolean; }\n            voteButton.addEventListener(\"click\", () => {\n                voteButton.classList.remove(\"icon-plus-square\", \"icon-plus-square-fill\");\n                voteButton.classList.add(\"spinner-border\", \"spinner-border-sm\");\n                fetch(PopForums.AreaPath + \"/Forum/ToggleVote/\" + this.postid, { method: \"POST\"})\n                .then(response => response.json()\n                    .then((result: resultType) => {\n                        this.votes = result.votes.toString();\n                        this.badge.innerHTML = \"+\" + this.votes;\n                        if (result.isVoted) {\n                            voteButton.classList.remove(\"spinner-border\", \"spinner-border-sm\");\n                            voteButton.classList.add(\"icon-plus-square-fill\");\n                        }\n                        else {\n                            voteButton.classList.remove(\"spinner-border\", \"spinner-border-sm\");\n                            voteButton.classList.add(\"icon-plus-square\");\n                        }\n                        this.applyPopover();\n                    }));\n            })\n        }\n        this.setupVoterPopover();\n        this.applyPopover();\n    }\n\n    private setupVoterPopover(): void {\n        this.voterContainer = document.createElement(\"div\");\n        if (this.votescontainerclass?.length > 0)\n            this.votescontainerclass.split(\" \").forEach((c) => this.voterContainer.classList.add(c));\n        this.voterContainer.innerHTML = `<div class=\"spinner-border\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div>`;\n        this.popOver = new bootstrap.Popover(this.badge, {\n            content: this.voterContainer,\n            html: true,\n            trigger: \"click focus\"\n        });\n        this.popoverEventHander = (e) => {\n            fetch(PopForums.AreaPath + \"/Forum/Voters/\" + this.postid)\n            .then(response => response.text()\n                .then(text => {\n                    let t = document.createElement(\"template\");\n                    t.innerHTML = text.trim();\n                    this.voterContainer.innerHTML = \"\";\n                    this.voterContainer.appendChild(t.content.firstChild!);\n                }));\n        };\n        this.badge.addEventListener(\"shown.bs.popover\", this.popoverEventHander);\n    }\n\n    private applyPopover(): void {\n        if (this.votes === \"0\") {\n            this.badge.style.cursor = \"default\";\n            this.popOver.disable();\n        }\n        else {\n            this.badge.style.cursor = \"pointer\";\n            this.popOver.enable();\n        }\n    }\n\n    private buttonGenerator(): string {\n        if (this.isloggedin === \"false\" || this.isauthor === \"true\")\n            return \"\";\n        if (this.isvoted === \"true\")\n            return VoteCount.cancelVoteButton;\n        return VoteCount.voteUpButton;\n    }\n\n    static template: string = `<div><div></div></div>`;\n\n    static voteUpButton = \"<span class=\\\"icon icon-plus-square\\\"></span>\";\n    static cancelVoteButton = \"<span class=\\\"icon icon-plus-square-fill\\\"></span>\";\n}\n\ncustomElements.define(\"pf-votecount\", VoteCount);\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/Declarations.ts",
    "content": "namespace PopForums {\n    export const AreaPath = \"/Forums\";\n    export var currentTopicState: TopicState;\n    export var currentForumState: ForumState;\n    export var currentPmState: PrivateMessageState;\n    export var userState: UserState;\n    export var localizations: Localizations;\n\n    export function Ready(callback: any): void {\n        if (document.readyState != \"loading\") callback();\n        else document.addEventListener(\"DOMContentLoaded\", callback);\n    }\n}\n\n\ndeclare namespace tinymce {\n    function init(options:any): any;\n    function get(id:string): any;\n    function triggerSave(): any;\n    let activeEditor: any;\n}\n\ndeclare class BlobInfo {\n    id: () => string;\n    name: () => string;\n    filename: () => string;\n    blob: () => Blob;\n    base64: () => string;\n    blobUri: () => string;\n    uri: () => string | undefined;\n} \n\ndeclare namespace bootstrap {\n    class Tooltip {\n        constructor(el: Element, options:any);\n    }\n    class Popover {\n        constructor(el: Element, options:any);\n        enable(): void;\n        disable(): void;\n    }\n    class Offcanvas extends HTMLElement {\n        constructor(el: Element);\n        show(): void;\n    }\n}\n\ndeclare namespace signalR {\n    enum HttpTransportType {\n        WebSockets = 1,\n        ServerSentEvents = 2,\n        LongPolling = 4\n    }\n    class HubConnectionBuilder {\n        withUrl(url: string, options?: { skipNegotiation?: boolean; transport?: HttpTransportType }): any;\n    }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/ElementBase.ts",
    "content": "﻿namespace PopForums {\n\nexport abstract class ElementBase<B extends StateBase> extends HTMLElement {\n\n    connectedCallback() {\n        if (this.state && this.propertyToWatch)\n            return;\n        let stateAndWatchProperty = this.getDependentReference();\n        this.state = stateAndWatchProperty[0];\n        this.propertyToWatch = stateAndWatchProperty[1];\n        const delegate = this.update.bind(this);\n        this.state.subscribe(this.propertyToWatch, delegate);\n    }\n\n    private state!: B;\n    private propertyToWatch!: string;\n\n    update() {\n        const externalValue = this.state[this.propertyToWatch as keyof B];\n        this.updateUI(externalValue);\n    }\n\n    // Implementation should return the StateBase and property (as a string) to monitor\n    abstract getDependentReference(): [B, string];\n\n    // Use in the implementation to manipulate the shadow or light DOM or straight markup as needed in response to the new data.\n    abstract updateUI(data: any): void;\n}\n\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Models/Notification.ts",
    "content": "namespace PopForums {\n\n    export class Notification {\n        userID!: number;\n        timeStamp!: Date;\n        isRead!: boolean;\n        notificationType!: number;\n        contextID!: number;\n        data: any;\n        unreadCount!: number;\n    }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Models/PrivateMessage.ts",
    "content": "namespace PopForums {\n\n    export class PrivateMessage {\n        pmPostID!: number;\n        userID!: number;\n        name!: string;\n        postTime!: Date;\n        fullText!: string;\n    }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Models/PrivateMessageUser.ts",
    "content": "namespace PopForums {\n    export class PrivateMessageUser {\n        userID!: number;\n        name!: string;\n    }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Services/LocalizationService.ts",
    "content": "namespace PopForums {\n\n    export class LocalizationService {\n        static init(): void {\n            const path = PopForums.AreaPath + \"/Resources\";\n            fetch(path)\n                .then(response => {\n                    return response.json();\n                })\n                .then(json => {\n                    PopForums.localizations = Object.assign(new Localizations(), json);\n                    return this.signal();\n                });\n        }\n\n        private static signal() {\n            PopForums.Ready(() => {\n                if (this.readies) {\n                    for (let i of this.readies) {\n                        i();\n                    }\n                }\n                this.isSignaled = true;\n            });\n        }\n\n        static readies: Array<Function>;\n        private static isSignaled: boolean = false;\n\n        static subscribe(ready: Function): boolean {\n            if (!this.readies)\n                this.readies = new Array<Function>();\n            this.readies.push(ready);\n            return this.isSignaled;\n        }\n    }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Services/MessagingService.ts",
    "content": "﻿namespace PopForums {\n\n\texport class MessagingService {\n\t\tprivate static service: MessagingService;\n\t\tprivate static promise: Promise<void>;\n\n\t\tstatic async GetService(): Promise<MessagingService> {\n\t\t\tif (!this.promise) {\n\t\t\t\tconst service = new MessagingService();\n\t\t\t\tthis.promise = service.start();\n\t\t\t\tthis.service = service;\n\t\t\t}\n\t\t\tawait Promise.all([this.promise]);\n\t\t\treturn this.service;\n\t\t}\n\n\t\tconnection: any;\n\n\t\tprivate async start() {\n\t\t\tthis.connection = new signalR.HubConnectionBuilder()\n\t\t\t\t.withUrl(\"/PopForumsHub\", { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets })\n\t\t\t\t.withAutomaticReconnect()\n\t\t\t\t.build();\n\t\t\tawait this.connection.start();\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/Services/NotificationService.ts",
    "content": "namespace PopForums {\n\n    export class NotificationService {\n        constructor(userState: UserState) {\n            this.userState = userState;\n        }\n\n        private userState: UserState;\n        private connection: any;\n\n        async initialize(): Promise<void> {\n            let self = this;\n            let service = await MessagingService.GetService();\n            this.connection = service.connection;\n            this.connection.on(\"updatePMCount\", function (pmCount: number) {\n                self.userState.newPmCount = pmCount;\n            });\n            this.connection.on(\"notify\", function (data: any) {\n                let notification: Notification = Object.assign(new Notification(), data);\n                let list = self.userState.list.querySelectorAll(\"pf-notificationitem\");\n                list.forEach(item => {\n                    let nitem = (item as NotificationItem).notification;\n                    if (nitem.contextID === notification.contextID && nitem.notificationType === notification.notificationType) {\n                        item.remove();\n                    }\n                });\n                self.userState.notificationCount = notification.unreadCount;\n                self.userState.notifications.unshift(notification);\n            });\n            this.connection.onreconnected(async () => {\n                let notificationCount = await this.connection.invoke(\"GetNotificationCount\");\n                self.userState.notificationCount = notificationCount;\n                let pmCount = await this.connection.invoke(\"GetPMCount\");\n                self.userState.newPmCount = pmCount;\n            });\n\t\t}\n\n        async LoadNotifications(): Promise<void> {\n            const json = await this.getNotifications();\n            let a = new Array<Notification>();\n            let isEnd = true;\n            json.forEach((item: Notification) => {\n                let n = Object.assign(new Notification(), item);\n                a.push(n);\n                this.userState.lastNotificationDate = n.timeStamp;\n                isEnd = false;\n            });\n            this.userState.isNotificationEnd = isEnd;\n            if (!isEnd)\n                this.userState.notifications = a;\n        }\n\n        async MarkRead(contextID: number, notificationType: number) : Promise<void> {\n            await this.connection.invoke(\"MarkNotificationRead\", contextID, notificationType);\n        }\n\n        async MarkAllRead() : Promise<void> {\n            await this.connection.send(\"MarkAllRead\");\n            let list = this.userState.list.querySelectorAll(\"pf-notificationitem\");\n            list.forEach(item => {\n                (item as NotificationItem).MarkRead();\n            });\n            this.userState.notificationCount = 0;\n        }\n\n        private async getNotifications() {\n            const response = await this.connection.invoke(\"GetNotifications\", this.userState.lastNotificationDate);\n            return response;\n        }\n    }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/State/ForumState.ts",
    "content": "namespace PopForums {\n\n    export class ForumState extends StateBase {\n        constructor() {\n            super();\n        }\n\n        forumID!: number;\n        pageSize!: number;\n        pageIndex!: number;\n        @WatchProperty\n        isNewTopicLoaded!: boolean;\n\n        setupForum() {\n            PopForums.Ready(async () => {\n                this.isNewTopicLoaded = false;\n                await this.forumListen();\n            });\n        }\n\n        loadNewTopic() {\n            fetch(PopForums.AreaPath + \"/Forum/PostTopic/\" + this.forumID)\n                .then((response) => {\n                    return response.text();\n                })\n                .then((body) => {\n                    var n = document.querySelector(\"#NewTopic\") as HTMLElement;\n                    if (!n)\n                        throw(\"Can't find a #NewTopic element to load in the new topic form.\");\n                    n.innerHTML = body;\n                    n.style.display = \"block\";\n                    this.isNewTopicLoaded = true;\n                });\n        }\n\n        async forumListen() {\n            let service = await MessagingService.GetService();\n            let connection = service.connection;\n            let self = this;\n            connection.on(\"notifyUpdatedTopic\", function (data: any) { // TODO: refactor to strong type\n                let removal = document.querySelector('#TopicList tr[data-topicID=\"' + data.topicID + '\"]');\n                if (removal) {\n                    removal.remove();\n                } else {\n                    let rows = document.querySelectorAll(\"#TopicList tr:not(#TopicTemplate)\");\n                    if (rows.length == self.pageSize)\n                        rows[rows.length - 1].remove();\n                }\n                let row = self.populateTopicRow(data);\n                row.classList.remove(\"hidden\");\n                document.querySelector(\"#TopicList tbody\")!.prepend(row);\n            });\n            await connection.invoke(\"listenToForum\", self.forumID);\n        }\n\n        async recentListen() {\n            let service = await MessagingService.GetService();\n            let connection = service.connection;\n            let self = this;\n            connection.on(\"notifyRecentUpdate\", function (data: any) {\n                var removal = document.querySelector('#TopicList tr[data-topicID=\"' + data.topicID + '\"]');\n                if (removal) {\n                    removal.remove();\n                } else {\n                    var rows = document.querySelectorAll(\"#TopicList tr:not(#TopicTemplate)\");\n                    if (rows.length == self.pageSize)\n                        rows[rows.length - 1].remove();\n                }\n                var row = self.populateTopicRow(data);\n                row.classList.remove(\"hidden\");\n                document.querySelector(\"#TopicList tbody\")!.prepend(row);\n            });\n            connection.invoke(\"listenRecent\");\n        }\n\n        populateTopicRow = function (data: any) {\n            let row = document.querySelector(\"#TopicTemplate\")!.cloneNode(true) as HTMLElement;\n            row.setAttribute(\"data-topicid\", data.topicID);\n            row.removeAttribute(\"id\");\n            row.querySelector(\".startedByName\")!.textContent = data.startedByName;\n            row.querySelector(\".indicatorLink\")!.setAttribute(\"href\", data.link);\n            row.querySelector(\".titleLink\")!.textContent = data.title;\n            row.querySelector(\".titleLink\")!.setAttribute(\"href\", data.link);\n            var forumTitle = row.querySelector(\".forumTitle\");\n            if (forumTitle) forumTitle.textContent = data.forumTitle;\n            row.querySelector(\".viewCount\")!.innerHTML = data.viewCount;\n            row.querySelector(\".replyCount\")!.innerHTML = data.replyCount;\n            row.querySelector(\".lastPostName\")!.textContent = data.lastPostName;\n            row.querySelector(\"pf-formattedtime\")!.setAttribute(\"utctime\", data.utc);\n            return row;\n        };\n    }\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/State/Localizations.ts",
    "content": "namespace PopForums {\n    export class Localizations {\n        todayTime!: string;\n        yesterdayTime!: string;\n        minutesAgo!: string;\n        oneMinuteAgo!: string;\n        lessThanMinute!: string;\n\n        notifications!: string;\n        newReplyNotification!: string;\n        award!: string;\n        voteUpNotification!: string;\n        questionAnsweredNotification!: string;\n        send!: string;\n\n        uploadImage!: string;\n    }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/State/PrivateMessageState.ts",
    "content": "namespace PopForums{\n\nexport class PrivateMessageState extends StateBase {\n    constructor() {\n        super();\n        this.isStart = false;\n    }\n\n    pmID!: number;\n    users!: PrivateMessageUser[];\n    messages!: PrivateMessage[];\n    newestPostID!: number;\n\n    private postStream!: HTMLElement;\n    private connection: any;\n    private isStart: boolean;\n\n    setupPm() {\n        PopForums.Ready(async () => {\n            this.postStream = document.getElementById(\"PostStream\")!;\n            this.messages.forEach(x => {\n                let messageRow = this.populateMessage(x);\n                this.postStream.append(messageRow);\n            });\n\n            let service = await MessagingService.GetService();\n            this.connection = service.connection;\n\n            let self = this;\n            this.connection.on(\"addMessage\", function(message: PrivateMessage) {\n                let messageRow = self.populateMessage(message);\n                let parent = self.postStream.parentElement!;\n                let isBottom = parent.scrollHeight - parent.scrollTop - parent.clientHeight < 200;\n                self.postStream.append(messageRow);\n                if (isBottom)\n                    parent.scrollTop = parent.scrollHeight;\n                self.ackRead();\n            });\n            this.connection.onreconnected(async () => {\n                let latestPostTime = this.messages[this.messages.length - 1].pmPostID;\n                const posts = await this.connection.invoke(\"GetMostRecentPmPosts\", this.pmID, latestPostTime) as PrivateMessage[];\n                posts.reverse().forEach((item: PrivateMessage) => {\n                    let m = this.populateMessage(item);\n                    this.postStream.append(m);\n                });\n            });\n            await this.connection.invoke(\"listenToPm\", this.pmID);\n            if (this.newestPostID) {\n                this.scrollToElement(\"p\" + this.newestPostID)\n            }\n            else {\n                this.postStream.parentElement!.scrollTop = this.postStream.parentElement!.scrollHeight;\n            }\n            await this.LoadCheck();\n            this.postStream.parentElement!.addEventListener(\"scroll\", this.ScrollLoad);\n        });\n    }\n\n    ScrollLoad = async () => {\n        await this.LoadCheck();\n    }\n\n    async LoadCheck() {\n        let box = this.postStream.parentElement!;\n        if (!this.isStart && box.scrollTop < 250) {\n            const posts = await this.GetPosts();\n            let isStart = true;\n            posts.reverse().forEach((item: PrivateMessage) => {\n                this.messages.unshift(item);\n                let m = this.populateMessage(item);\n                this.postStream.prepend(m);\n                isStart = false;\n            });\n            this.isStart = isStart;\n        }\n    }\n\n    private async GetPosts() {\n        let earliestPostTime = this.messages[0].postTime;\n        const response = await this.connection.invoke(\"GetPmPosts\", this.pmID, earliestPostTime) as PrivateMessage[];\n        return response;\n    }\n\n    send(fullText: string) {\n        if (!fullText || fullText.trim().length === 0)\n            return;\n        this.connection.invoke(\"sendPm\", this.pmID, fullText);\n    }\n\n    ackRead() {\n        this.connection.invoke(\"ackReadPm\", this.pmID);\n    }\n\n    populateMessage(data: PrivateMessage) {\n        let template = document.createElement(\"template\");\n        template.innerHTML = PrivateMessageState.template;\n        let messageRow = template.content.cloneNode(true) as HTMLElement;\n\n        let body = messageRow.querySelector(\"div > div\")!;\n        body.innerHTML = data.fullText;\n        if (data.userID === PopForums.userState.userID) {\n            body.classList.add(\"alert-secondary\");\n            messageRow.querySelector(\"div\")!.classList.add(\"ms-auto\");\n        }\n        else\n            body.classList.add(\"alert-primary\");\n\n        let timeStamp = messageRow.querySelector(\"pf-formattedtime\")!;\n        timeStamp.setAttribute(\"utctime\", data.postTime.toString());\n\n        let name = messageRow.querySelector(\".messageName\")!;\n        name.innerHTML = data.name;\n\n        body.parentElement!.id = \"p\" + data.pmPostID;\n\n        return messageRow;\n    };\n\n    scrollToElement = (id: string) => {\n        let e = document.getElementById(id) as HTMLElement;\n        e.scrollIntoView();\n    };\n\n    static template: string =\n`<div class=\"w-75 mb-3\">\n    <span class=\"d-flex\">\n        <small class=\"messageName me-3\"></small>\n        <small class=\"ms-auto\"><pf-formattedtime utctime=\"\"></pf-formattedtime></small>\n    </span>\n    <div class=\"alert\">\n\n    </div>\n</div>`;\n}\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/State/TopicState.ts",
    "content": "namespace PopForums {\n\nexport class TopicState extends StateBase {\n    constructor() {\n        super();\n    }\n\n    topicID!: number;\n    isImageEnabled!: boolean;\n    @WatchProperty\n    isReplyLoaded!: boolean;\n    @WatchProperty\n    answerPostID!: number;\n    @WatchProperty\n\tlowPage!:number;\n    @WatchProperty\n\thighPage!: number;\n\tlastVisiblePostID!: number;\n    @WatchProperty\n    isNewerPostsAvailable!: boolean;\n    pageIndex!: number;\n\tpageCount!: number;\n\tloadingPosts: boolean = false;\n\tisScrollAdjusted: boolean = false;\n    @WatchProperty\n    commentReplyID!: number;\n    @WatchProperty\n    nextQuote!: string;\n    @WatchProperty\n    isSubscribed!: boolean;\n    @WatchProperty\n    isFavorite!: boolean;\n    documentFragment!: DocumentFragment;\n    selectionAncestor!: Node;\n\n    setupTopic() {\n        PopForums.Ready(async () => {\n            this.isReplyLoaded = false;\n            this.isNewerPostsAvailable = false;\n            this.lowPage = this.pageIndex;\n            this.highPage = this.pageIndex;\n\n            // signalR connections\n            let service = await MessagingService.GetService();\n            let connection = service.connection;\n            let self = this;\n            // for all posts loaded but reply not open\n            connection.on(\"fetchNewPost\", function (postID: number) {\n                if (!self.isReplyLoaded && self.highPage === self.pageCount) {\n                    fetch(PopForums.AreaPath + \"/Forum/Post/\" + postID)\n                        .then(response => response.text()\n                            .then(text => {\n                                var t = document.createElement(\"template\");\n                                t.innerHTML = text.trim();\n                                document.querySelector(\"#PostStream\")!.appendChild(t.content.firstChild!);\n                            }));\n                    self.lastVisiblePostID = postID;\n                }\n            });\n            // for reply already open\n            connection.on(\"notifyNewPosts\", function (theLastPostID: number) {\n                self.setMorePostsAvailable(theLastPostID);\n            });\n            connection.invoke(\"listenToTopic\", this.topicID);\n            this.connection = connection;\n\n            document.querySelectorAll(\".postItem img:not(.avatar)\").forEach(x => x.classList.add(\"postImage\"));\n\n            this.scrollToPostFromHash();\n            window.addEventListener(\"scroll\", this.scrollLoad);\n\n            // compensate for iOS losing selection when you touch the quote button\n            document.querySelectorAll(\".postBody\").forEach( x => x.addEventListener(\"touchend\", (e) => {\n                let selection = document.getSelection();\n                if (!selection || selection.rangeCount === 0 || selection.getRangeAt(0).toString().length === 0) {\n                    return;\n                }\n                let range = selection.getRangeAt(0);\n                this.selectionAncestor = range.commonAncestorContainer;\n                this.documentFragment = range.cloneContents();\n            }));\n        });\n    }\n\n    loadReply(topicID:number, replyID:number, setupMorePosts:boolean):void {\n        if (this.isReplyLoaded) {\n            this.scrollToElement(\"NewReply\");\n            return;\n        }\n        window.removeEventListener(\"scroll\", this.scrollLoad);\n        var path = PopForums.AreaPath + \"/Forum/PostReply/\" + topicID;\n        if (replyID != null) {\n            path += \"?replyID=\" + replyID;\n        }\n\n        fetch(path)\n            .then(response => response.text()\n                .then(text => {\n                    let n = document.querySelector(\"#NewReply\") as HTMLElement;\n                    n!.innerHTML = text;\n                    n!.style.display = \"block\";\n                    this.scrollToElement(\"NewReply\");\n                    this.isReplyLoaded = true;\n\n                    if (setupMorePosts) {\n                        let self = this;\n                        this.connection.invoke(\"getLastPostID\", this.topicID)\n                        .then(function (result: number) {\n                            self.setMorePostsAvailable(result);\n                        });\n                    }\n                    this.isReplyLoaded = true;\n                    this.commentReplyID = 0;\n                }));\n    }\n\n    private connection: any;\n\n    // this is intended to be called when the reply box is open\n    private setMorePostsAvailable = (newestPostIDonServer: number) => {\n        this.isNewerPostsAvailable = newestPostIDonServer !== this.lastVisiblePostID;\n    }\n\n    loadComment(topicID: number, replyID: number): void {\n        var n = document.querySelector(\"[data-postid*='\" + replyID + \"'] .commentHolder\");\n        const boxid = \"commentbox\";\n        n!.id = boxid;\n        var path = PopForums.AreaPath + \"/Forum/PostReply/\" + topicID + \"?replyID=\" + replyID;\n        this.commentReplyID = replyID;\n        this.isReplyLoaded = true;\n        fetch(path)\n            .then(response => response.text()\n                .then(text => {\n                    n!.innerHTML = text;\n                    this.scrollToElement(boxid);\n                }));\n    };\n\n    loadMorePosts = () => {\n        let topicPagePath: string;\n        if (this.highPage === this.pageCount) {\n            topicPagePath = PopForums.AreaPath + \"/Forum/TopicPartial/\" + this.topicID + \"?lastPost=\" + this.lastVisiblePostID + \"&lowPage=\" + this.lowPage;\n        }\n        else {\n            this.highPage++;\n            topicPagePath = PopForums.AreaPath + \"/Forum/TopicPage/\" + this.topicID + \"?pageNumber=\" + this.highPage + \"&low=\" + this.lowPage + \"&high=\" + this.highPage;\n        }\n        fetch(topicPagePath)\n            .then(response => response.text()\n                .then(text => {\n                    let t = document.createElement(\"template\");\n                    t.innerHTML = text.trim();\n                    let stuff = t.content.firstChild as HTMLElement;\n                    let links = stuff.querySelector(\".pagerLinks\");\n                    if (links)\n                        stuff.removeChild(links);\n                    let lastPostID = stuff.querySelector(\".lastPostID\") as HTMLInputElement;\n                    stuff.removeChild(lastPostID);\n                    let newPageCount = stuff.querySelector(\".pageCount\") as HTMLInputElement;\n                    stuff.removeChild(newPageCount);\n                    this.lastVisiblePostID = Number(lastPostID.value);\n                    this.pageCount = Number(newPageCount.value);\n                    let postStream = document.querySelector(\"#PostStream\")!;\n                    postStream.append(stuff);\n                    document.querySelectorAll(\".pagerLinks\").forEach(x => x.replaceWith(links!.cloneNode(true)));\n                    document.querySelectorAll(\".postItem img:not(.avatar)\").forEach(x => x.classList.add(\"postImage\"));\n                    if (this.highPage == this.pageCount && this.lowPage == 1) {\n                        document.querySelectorAll(\".pagerLinks\").forEach(x => x.remove());\n                    }\n                    this.loadingPosts = false;\n                    if (!this.isScrollAdjusted) {\n                        this.scrollToPostFromHash();\n                    }\n                    if (this.isReplyLoaded) {\n                        let self = this;\n                        this.connection.invoke(\"getLastPostID\", this.topicID)\n                        .then(function (result: number) {\n                            self.setMorePostsAvailable(result);\n                        });\n                    }\n                }));\n    };\n\n    loadPreviousPosts = () => {\n        this.lowPage--;\n        let topicPagePath = PopForums.AreaPath + \"/Forum/TopicPage/\" + this.topicID + \"?pageNumber=\" + this.lowPage + \"&low=\" + this.lowPage + \"&high=\" + this.highPage;\n        fetch(topicPagePath)\n            .then(response => response.text()\n                .then(text => {\n                    let t = document.createElement(\"template\");\n                    t.innerHTML = text.trim();\n                    var stuff = t.content.firstChild as HTMLElement;\n                    var links = stuff.querySelector(\".pagerLinks\") as Element;\n                    stuff.removeChild(links);\n                    var postStream = document.querySelector(\"#PostStream\")!;\n                    postStream.prepend(stuff);\n                    document.querySelectorAll(\".pagerLinks\").forEach(x => x.replaceWith(links.cloneNode(true)));\n                    document.querySelectorAll(\".postItem img:not(.avatar)\").forEach(x => x.classList.add(\"postImage\"));\n                    if (this.highPage == this.pageCount && this.lowPage == 1) {\n                        document.querySelectorAll(\".pagerLinks\").forEach(x => x.remove());\n                    }\n                }));\n    }\n\n    scrollLoad = () => {\n        let streamEnd = (document.querySelector(\"#StreamBottom\") as HTMLElement);\n        if (!streamEnd)\n            return; // this is a QA topic, no continuous post stream\n        let top = streamEnd.offsetTop;\n        let viewEnd = window.scrollY + window.outerHeight;\n        let distance = top - viewEnd;\n        if (!this.loadingPosts && distance < 250 && this.highPage < this.pageCount) {\n            this.loadingPosts = true;\n            this.loadMorePosts();\n        }\n    };\n\n    scrollToElement = (id: string) => {\n        let e = document.getElementById(id) as HTMLElement;\n        let t = 0;\n        if (e.offsetParent) {\n            while (e.offsetParent) {\n                t += e.offsetTop;\n                e = e.offsetParent as HTMLElement;\n            }\n        } else if (e.getBoundingClientRect().y) {\n            t += e.getBoundingClientRect().y;\n        }\n        let crumb = document.querySelector(\"#TopBreadcrumb\") as HTMLElement;\n        if (crumb)\n            t -= crumb.offsetHeight;\n        scrollTo(0, t);\n    };\n\n    scrollToPostFromHash = () => {\n        if (window.location.hash) {\n            Promise.all(Array.from(document.querySelectorAll(\"#PostStream img\"))\n                .filter(img => !(img as HTMLImageElement).complete)\n                .map(img => new Promise(resolve => { (img as HTMLImageElement).onload = (img as HTMLImageElement).onerror = resolve; })))\n                    .then(() => {\n                        let hash = window.location.hash;\n                        while (hash.charAt(0) === '#') hash = hash.substring(1);\n                        let tag = document.querySelector(\"div[data-postID='\" + hash + \"']\");\n                        if (tag) {\n                            let tagPosition = tag.getBoundingClientRect().top;\n                            let crumb = document.querySelector(\"#ForumContainer #TopBreadcrumb\")!;\n                            let crumbHeight = crumb.getBoundingClientRect().height;\n                            let e = getComputedStyle(document.querySelector(\".postItem\") as Element);\n                            let margin = parseFloat(e.marginTop);\n                            let newPosition = tagPosition - crumbHeight - margin;\n                            window.scrollBy({ top: newPosition, behavior: 'auto' });\n                        }\n                        this.isScrollAdjusted = true;\n                    });\n        }\n    };\n\n    setAnswer(postID: number, topicID: number) {\n        var model = { postID: postID, topicID: topicID };\n        fetch(PopForums.AreaPath + \"/Forum/SetAnswer/\", {\n            method: \"POST\",\n            body: JSON.stringify(model),\n            headers: {\n                \"Content-Type\": \"application/json\"\n            }\n        })\n            .then(response => {\n                this.answerPostID = postID;\n            });\n    }\n}\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/State/UserState.ts",
    "content": "namespace PopForums {\n\nexport class UserState extends StateBase {\n    constructor() {\n        super();\n    }\n\n    private notificationService!: NotificationService;\n    private isLoadingNotifications!: boolean;\n\n    isPlainText!: boolean;\n    isImageEnabled!: boolean;\n    postImageIds!: Array<string>;\n    userID!: number;\n    lastNotificationDate!: Date;\n    isNotificationEnd!: boolean;\n\n    @WatchProperty\n    newPmCount!: number;\n    @WatchProperty\n    notificationCount!: number;\n    @WatchProperty\n    notifications!: Array<Notification>;\n\n    list!: HTMLElement;\n\n    async initialize(): Promise<void> {\n        this.postImageIds = new Array<string>();\n        this.notificationService = new NotificationService(this);\n        await this.notificationService.initialize();\n\t}\n\n    async LoadNotifications(): Promise<void> {\n        this.isLoadingNotifications = true;\n        this.lastNotificationDate = new Date(2100, 1, 1);\n        this.isNotificationEnd = false;\n        this.notifications = new Array<Notification>();\n        await this.notificationService.LoadNotifications();\n        this.isLoadingNotifications = false;\n    }\n\n    async MarkRead(contextID: number, notificationType: number) : Promise<void> {\n        await this.notificationService.MarkRead(contextID, notificationType);\n    }\n\n    async MarkAllRead() : Promise<void> {\n        await this.notificationService.MarkAllRead();\n    }\n\n    ScrollLoad = async () => {\n        if (this.isNotificationEnd)\n            return;\n        let streamEnd = (document.querySelector(\"#NotificationBottom\") as HTMLElement);\n        if (!streamEnd) {\n            console.log(\"Can't find bottom of notifications.\");\n            return;\n        }\n        let top = streamEnd.offsetTop;\n        let viewEnd = this.list.scrollTop + this.list.clientHeight;\n        let distance = top - viewEnd;\n        if (!this.isLoadingNotifications && distance < 250 && !this.isNotificationEnd) {\n            await this.LoadMoreNotifications();\n        }\n    };\n\n    private async LoadMoreNotifications() {\n        this.isLoadingNotifications = true;\n        await this.notificationService.LoadNotifications();\n        this.isLoadingNotifications = false;\n    }\n}\n\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/Client/StateBase.ts",
    "content": "﻿namespace PopForums {\n\n// Properties to watch require the @WatchProperty attribute.\nexport class StateBase {\n    constructor() {\n        this._subs = new Map<string, Array<Function>>();\n    }\n\n    private _subs: Map<string, Array<Function>>;\n\n    subscribe(propertyName: string, eventHandler: Function) {\n        if (!this._subs.has(propertyName))\n            this._subs.set(propertyName, new Array<Function>());\n        const callbacks = this._subs.get(propertyName)!;\n        callbacks.push(eventHandler);\n        eventHandler();\n    }\n\n    notify(propertyName: string) {\n        const callbacks = this._subs.get(propertyName);\n        if (callbacks)\n            for (let i of callbacks) {\n                i();\n            }\n    }\n}\n\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/WatchPropertyAttribute.ts",
    "content": "﻿namespace PopForums {\n\nexport const WatchProperty = (target: any, memberName: string) => {\n    let currentValue: any = target[memberName];  \n    Object.defineProperty(target, memberName, {\n        set(this: any, newValue: any) {\n            currentValue = newValue;\n            this.notify(memberName);\n        },\n        get() {return currentValue;}\n    });\n};\n\n}"
  },
  {
    "path": "src/PopForums.Mvc/Client/tsconfig.json",
    "content": "{\n  \"compileOnSave\": true,\n  \"compilerOptions\": {\n    \"target\": \"es2018\",\n    \"ignoreDeprecations\": \"6.0\",\n    \"inlineSources\": true,\n    \"inlineSourceMap\": true,\n    \"outFile\": \"../wwwroot/PopForums.js\",\n    \"experimentalDecorators\": true,\n    \"noImplicitAny\": true,\n    \"moduleResolution\": \"Node\"\n  }\n}"
  },
  {
    "path": "src/PopForums.Mvc/Global.cs",
    "content": "﻿global using System;\nglobal using System.Collections.Generic;\nglobal using System.Globalization;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Net;\nglobal using System.Reflection;\nglobal using System.Security;\nglobal using System.Security.Claims;\nglobal using System.Text;\nglobal using System.Text.Encodings.Web;\nglobal using System.Text.Json;\nglobal using System.Threading.Tasks;\n\nglobal using Microsoft.AspNetCore.Authentication;\nglobal using Microsoft.AspNetCore.Authorization;\nglobal using Microsoft.AspNetCore.Builder;\nglobal using Microsoft.AspNetCore.DataProtection;\nglobal using Microsoft.AspNetCore.Html;\nglobal using Microsoft.AspNetCore.Http;\nglobal using Microsoft.AspNetCore.Localization;\nglobal using Microsoft.AspNetCore.Mvc;\nglobal using Microsoft.AspNetCore.Mvc.Controllers;\nglobal using Microsoft.AspNetCore.Mvc.Filters;\nglobal using Microsoft.AspNetCore.Mvc.ModelBinding;\nglobal using Microsoft.AspNetCore.Mvc.Rendering;\nglobal using Microsoft.AspNetCore.Mvc.TagHelpers;\nglobal using Microsoft.AspNetCore.Mvc.ViewFeatures;\nglobal using Microsoft.AspNetCore.Razor.TagHelpers;\nglobal using Microsoft.AspNetCore.Routing;\nglobal using Microsoft.AspNetCore.SignalR;\nglobal using Microsoft.Extensions.DependencyInjection;\nglobal using Microsoft.Extensions.Logging;\n\nglobal using PopForums.Configuration;\nglobal using PopForums.Email;\nglobal using PopForums.Extensions;\nglobal using PopForums.ExternalLogin;\nglobal using PopForums.Feeds;\nglobal using PopForums.Messaging;\nglobal using PopForums.Models;\nglobal using PopForums.Repositories;\nglobal using PopForums.ScoringGame;\nglobal using PopForums.Services;\n\nglobal using PopForums.Mvc.Areas.Forums.Authorization;\nglobal using PopForums.Mvc.Areas.Forums.Controllers;\nglobal using PopForums.Mvc.Areas.Forums.Extensions;\nglobal using PopForums.Mvc.Areas.Forums.Messaging;\nglobal using PopForums.Mvc.Areas.Forums.Models;\nglobal using PopForums.Mvc.Areas.Forums.Services;"
  },
  {
    "path": "src/PopForums.Mvc/PopForums.Mvc.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Razor\">\n\n\t<PropertyGroup>\n\t\t<Description>PopForums Mvc Class Library</Description>\n\t\t<VersionPrefix>22.0.0</VersionPrefix>\n\t\t<Authors>Jeff Putz</Authors>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<AssemblyName>PopForums.Mvc</AssemblyName>\n\t\t<PackageId>PopForums.Mvc</PackageId>\n\t\t<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>\n\t\t<AddRazorSupportForMvc>true</AddRazorSupportForMvc>\n\t\t<PackageProjectUrl>https://github.com/POPWorldMedia/POPForums</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/POPWorldMedia/POPForums</RepositoryUrl>\n\t\t<Copyright>2025, POP World Media, LLC</Copyright>\n\t\t<PackageLicenseExpression>MIT</PackageLicenseExpression>\n\t\t<!--This is an important line to trigger the hot reload:-->\n\t\t<AssignTargetPathsDependsOn>CompileTypeScript;$(AssignTargetPathsDependsOn)</AssignTargetPathsDependsOn>\n\t\t<DefineStaticWebAssetsDependsOn>CompileTypeScript;$(DefineStaticWebAssetsDependsOn)</DefineStaticWebAssetsDependsOn>\n\t\t<ResolveStaticWebAssetsInputsDependsOn>CompileTypeScript;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>\n\t\t<StaticWebAssetBasePath>/PopForums</StaticWebAssetBasePath>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\n\t\t<PackageReference Include=\"Microsoft.TypeScript.MSBuild\" Version=\"6.0.3\">\n\t\t  <PrivateAssets>all</PrivateAssets>\n\t\t  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n\t\t</PackageReference>\n\t\t<PackageReference Include=\"PopIdentity\" Version=\"4.5.0\" />\n\t\t<ProjectReference Include=\"..\\PopForums\\PopForums.csproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t  <TypeScriptCompile Remove=\"node_modules\\**\" />\n\t</ItemGroup>\n\n\t<!--necessary to prevent pack in nuget package-->\n\t<ItemGroup>\n\t\t<Content Remove=\"Client\\tsconfig.json\" />\n\t\t<Content Remove=\"package-lock.json\" />\n\t\t<Content Remove=\"package.json\" />\n\t\t<Content Remove=\"wwwroot\\PopForums.js\" />\n\t</ItemGroup>\n\n\t<!--Include generated TypeScript output after compilation-->\n\t<ItemGroup>\n\t\t<Content Include=\"wwwroot\\PopForums.js\" Condition=\"Exists('wwwroot\\PopForums.js')\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t  <Folder Include=\"wwwroot\\lib\\\" />\n\t</ItemGroup>\n\n\t<!--necessary to prevent pack in nuget package-->\n\t<ItemGroup>\n\t  <TypeScriptCompile Include=\"Client\\tsconfig.json\" />\n\t  <None Include=\"package-lock.json\" />\n\t  <None Include=\"package.json\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/PopForums.Mvc/gulpfile.js",
    "content": "﻿/// <binding BeforeBuild=\"default\" />\n\nvar gulp = require(\"gulp\"),\n\tmerge = require(\"merge-stream\"),\n\tbabel = require(\"gulp-babel\"),\n\tcleancss = require(\"gulp-clean-css\"),\n\tuglify = require(\"gulp-uglify\"),\n\tsourcemaps = require(\"gulp-sourcemaps\"),\n\trename = require(\"gulp-rename\"),\n\ttypescript = require(\"gulp-typescript\");\n\nvar project = typescript.createProject(\"Client/tsconfig.json\")\nvar nodeRoot = \"./node_modules/\";\nvar targetPath = \"./wwwroot/lib/\";\n\ngulp.task(\"ts\", function () {\n\treturn project.src().pipe(project()).js.pipe(gulp.dest(\"wwwroot\"));\n});\n\ngulp.task(\"copies\", function () {\n\tvar streams = [\n\t\tgulp.src(nodeRoot + \"bootstrap/dist/js/bootstrap.bundle.*\").pipe(gulp.dest(targetPath + \"/bootstrap/dist/js\")),\n\t\tgulp.src(nodeRoot + \"bootstrap/dist/css/bootstrap.css\").pipe(gulp.dest(targetPath + \"/bootstrap/dist/css\")),\n\t\tgulp.src(nodeRoot + \"bootstrap/dist/css/bootstrap.css.map\").pipe(gulp.dest(targetPath + \"/bootstrap/dist/css\")),\n\t\tgulp.src(nodeRoot + \"bootstrap/dist/css/bootstrap.min.css\").pipe(gulp.dest(targetPath + \"/bootstrap/dist/css\")),\n\t\tgulp.src(nodeRoot + \"bootstrap/dist/css/bootstrap.min.css.map\").pipe(gulp.dest(targetPath + \"/bootstrap/dist/css\")),\n\t\tgulp.src(nodeRoot + \"@microsoft/signalr/dist/browser/**/*\").pipe(gulp.dest(targetPath + \"/signalr/dist\")),\n\t\tgulp.src(nodeRoot + \"tinymce/**/*\").pipe(gulp.dest(targetPath + \"/tinymce\")),\n\t\tgulp.src(nodeRoot + \"vue/dist/vue.global.*\").pipe(gulp.dest(targetPath + \"/vue/dist\")),\n\t\tgulp.src(nodeRoot + \"vue-router/dist/vue-router.global.*\").pipe(gulp.dest(targetPath + \"/vue-router/dist\")),\n\t\tgulp.src(nodeRoot + \"axios/dist/**/*\").pipe(gulp.dest(targetPath + \"/axios/dist\")),\n\t\tgulp.src(\"./wwwroot/Fonts/**/*\").pipe(gulp.dest(targetPath + \"/PopForums/dist/Fonts\"))\n\t];\n\treturn merge(streams);\n});\n\nfunction jsTask() {\n\treturn gulp.src(\"./wwwroot/*.js\", { allowEmpty: true })\n        .pipe(gulp.dest(targetPath + \"/PopForums/dist\"))\n\t\t.pipe(sourcemaps.init({ loadMaps: true }))\n\t\t.pipe(babel({ presets: [\"@babel/preset-env\"], sourceMap: true }))\n\t\t.pipe(uglify())\n\t\t.pipe(rename({ suffix: '.min' }))\n\t\t.pipe(sourcemaps.write(\"./\"))\n\t\t.pipe(gulp.dest(targetPath + \"/PopForums/dist\"));\n}\n\nfunction cssTask() {\n\treturn gulp.src(\"./wwwroot/*.css\", { allowEmpty: true })\n\t\t.pipe(gulp.dest(targetPath + \"/PopForums/dist\"))\n        .pipe(sourcemaps.init())\n\t\t.pipe(cleancss())\n\t\t.pipe(rename({ suffix: '.min' }))\n        .pipe(sourcemaps.write(\"./\"))\n\t\t.pipe(gulp.dest(targetPath + \"/PopForums/dist\"));\n}\n\ngulp.task(\"js\", jsTask);\ngulp.task(\"css\", cssTask);\n\ngulp.task(\"default\", gulp.series([\"ts\",\"copies\",\"js\",\"css\"]));"
  },
  {
    "path": "src/PopForums.Mvc/package.json",
    "content": "{\n  \"scripts\": {},\n  \"dependencies\": {\n    \"@microsoft/signalr\": \"10.0.0\",\n    \"axios\": \"1.15.2\",\n    \"bootstrap\": \"5.3.8\",\n    \"tinymce\": \"8.4.0\",\n    \"vue\": \"3.5.33\",\n    \"vue-router\": \"5.0.6\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"6.0.3\",\n    \"gulp-typescript\": \"5.0.1\",\n    \"@babel/core\": \"7.28.5\",\n    \"@babel/preset-env\": \"7.28.5\",\n    \"gulp\": \"4.0.2\",\n    \"gulp-babel\": \"8.0.0\",\n    \"gulp-clean-css\": \"4.3.0\",\n    \"gulp-rename\": \"2.1.0\",\n    \"gulp-sourcemaps\": \"3.0.0\",\n    \"gulp-uglify\": \"3.0.2\",\n    \"merge-stream\": \"2.0.0\"\n  },\n  \"babel\": {\n    \"presets\": [\n      \"@babel/preset-env\"\n    ]\n  }\n}\n"
  },
  {
    "path": "src/PopForums.Mvc/wwwroot/Admin.js",
    "content": "﻿const basePath = \"/Forums/AdminApi/\";\n\nconst Top = {\n\ttemplate: \"#Top\",\n\tdata() {\n\t\treturn {\n\t\t\tloading: true\n\t\t}\n\t},\n\tmounted: function () {\n\n\t},\n\tmethods: {\n\t\tsetLoading: function (isLoading) {\n\t\t\tthis.loading = isLoading;\n\t\t}\n\t}\n}\n\nvar loadingMixin = {\n\tdata() {\n\t\treturn {\n\t\t\talert: false,\n\t\t\tmessage: \"\"\n\t\t}\n\t},\n\tprops: {\n\t\tloading: Boolean\n\t},\n\tmethods: {\n\t\tstartLoad: function () {\n\t\t\tthis.$emit(\"setLoading\", true);\n\t\t},\n\t\tendLoad: function (message) {\n\t\t\tthis.$emit(\"setLoading\", false);\n\t\t\tif (message) {\n\t\t\t\tthis.alert = true;\n\t\t\t\tthis.message = message;\n\t\t\t\tvar c = this;\n\t\t\t\tsetTimeout(function () { c.alert = false; }, 5000);\n\t\t\t}\n\t\t},\n\t\terrorAlert: function() {\n\t\t\talert(\"There was an error. Please reload admin.\");\n\t\t}\n\t}\n}\nvar settingsMixin = {\n\tdata() {\n\t\treturn {\n\t\t\tsettings: {}\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.startLoad();\n\t\taxios.get(basePath + \"GetSettings\").then(response => {\n\t\t\tthis.settings = response.data;\n\t\t\tthis.endLoad();\n\t\t})\n\t\t\t.catch(error => this.errorAlert());\n\t},\n\tmethods: {\n\t\tsave: function (message) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"SaveSettings\", this.settings).then(response => {\n\t\t\t\tthis.settings = response.data;\n\t\t\t\tthis.endLoad(message);\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n\n}\n\nconst General = {\n\tmixins: [settingsMixin, loadingMixin],\n\ttemplate: \"#General\"\n}\n\nconst Categories = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#Categories\",\n\tdata() {\n\t\treturn {\n\t\t\tcategories: [],\n\t\t\tnewCategory: \"\",\n\t\t\teditCategory: \"\",\n\t\t\teditID: 0\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.startLoad();\n\t\taxios.get(basePath + \"GetCategories\").then(response => {\n\t\t\tthis.categories = response.data;\n\t\t\tthis.endLoad();\n\t\t})\n\t\t\t.catch(error => this.errorAlert());\n\t},\n\tmethods: {\n\t\tsaveNew: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"AddCategory\", { title: this.newCategory }).then(response => {\n\t\t\t\tthis.categories = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t\tthis.newCategory = \"\";\n\t\t},\n\t\tdeleteCat: function (item) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"DeleteCategory/\" + item.categoryID).then(response => {\n\t\t\t\tthis.categories = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tup: function (item) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"MoveCategoryUp/\" + item.categoryID).then(response => {\n\t\t\t\tthis.categories = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tdown: function (item) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"MoveCategoryDown/\" + item.categoryID).then(response => {\n\t\t\t\tthis.categories = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\teditCat: function (item) {\n\t\t\tthis.editCategory = item.title;\n\t\t\tthis.editID = item.categoryID;\n\t\t\tconst e = this.$refs.modal;\n\t\t\tvar modal = new bootstrap.Modal(e);\n\t\t\tmodal.show();\n\t\t},\n\t\tsaveCat: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"EditCategory\", { categoryID: this.editID, title: this.editCategory })\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.categories = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst Forums = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#Forums\",\n\tdata() {\n\t\treturn {\n\t\t\tcategories: [],\n\t\t\teditingForum: null\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.startLoad();\n\t\taxios.get(basePath + \"GetForums\").then(response => {\n\t\t\tthis.categories = response.data;\n\t\t\tthis.endLoad();\n\t\t})\n\t\t\t.catch(error => this.errorAlert());\n\t\tthis.resetForum();\n\t},\n\tmethods: {\n\t\tup: function (forum) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"MoveForumUp/\" + forum.forumID).then(response => {\n\t\t\t\tthis.categories = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tdown: function (forum) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"MoveForumDown/\" + forum.forumID).then(response => {\n\t\t\t\tthis.categories = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\teditForum: function (forum) {\n\t\t\tthis.editingForum = forum;\n\t\t\tif (!this.editingForum.categoryID)\n\t\t\t\tthis.editingForum.categoryID = 0;\n\t\t\tconst e = this.$refs.modal;\n\t\t\tvar modal = new bootstrap.Modal(e);\n\t\t\tmodal.show();\n\t\t},\n\t\tresetForum: function () {\n\t\t\tthis.editingForum = { forumID: 0, title: \"\", description: \"\", categoryID: 0, isVisible: true, isArchived: false, isQAForum: false, forumAdapterName: null };\n\t\t},\n\t\tnewForum: function () {\n\t\t\tthis.resetForum();\n\t\t\tconst e = this.$refs.modal;\n\t\t\tvar modal = new bootstrap.Modal(e);\n\t\t\tmodal.show();\n\t\t},\n\t\tsaveForum: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"SaveForum\", this.editingForum)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.categories = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst ForumPermissions = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#ForumPermissions\",\n\tdata() {\n\t\treturn {\n\t\t\tcategories: [],\n\t\t\tselectedForum: null,\n\t\t\tallRoles: [],\n\t\t\tpostRoles: [],\n\t\t\tviewRoles: [],\n\t\t\tselectedAll: null,\n\t\t\tselectedPost: null,\n\t\t\tselectedView: null\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.startLoad();\n\t\taxios.get(basePath + \"GetForums\").then(response => {\n\t\t\tthis.categories = response.data;\n\t\t\tthis.endLoad();\n\t\t})\n\t\t\t.catch(error => this.errorAlert());\n\t\tthis.updatePerm();\n\t},\n\tmethods: {\n\t\tforumChange: function () {\n\t\t\tthis.updatePerm();\n\t\t},\n\t\tupdatePerm: function () {\n\t\t\tif (this.selectedForum) {\n\t\t\t\tthis.startLoad();\n\t\t\t\taxios.get(basePath + \"GetForumPermissions/\" + this.selectedForum).then(response => {\n\t\t\t\t\tthis.allRoles = response.data.allRoles;\n\t\t\t\t\tthis.viewRoles = response.data.viewRoles;\n\t\t\t\t\tthis.postRoles = response.data.postRoles;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t\t.catch(error => this.errorAlert());\n\t\t\t}\n\t\t},\n\t\tmodify: function (modifyType, role) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"ModifyForumRoles\", { forumID: this.selectedForum, modifyType: modifyType, role: role }).then(response => {\n\t\t\t\tthis.endLoad();\n\t\t\t\tthis.updatePerm();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst Email = {\n\tmixins: [settingsMixin, loadingMixin],\n\ttemplate: \"#Email\"\n}\n\nconst Search = {\n\tmixins: [settingsMixin, loadingMixin],\n\ttemplate: \"#Search\",\n\tdata() {\n\t\treturn {\n\t\t\tjunkWords: [],\n\t\t\tselectedWord: null,\n\t\t\tnewWord: \"\"\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.updateJunk();\n\t},\n\tmethods: {\n\t\tupdateJunk: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.get(basePath + \"GetJunkWords\").then(response => {\n\t\t\t\tthis.junkWords = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tcreateWord: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"CreateJunkWord/\" + this.newWord).then(response => {\n\t\t\t\tthis.endLoad();\n\t\t\t\tthis.newWord = \"\";\n\t\t\t\tthis.updateJunk();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tdeleteWord: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"DeleteJunkWord/\" + this.selectedWord).then(response => {\n\t\t\t\tthis.endLoad();\n\t\t\t\tthis.updateJunk();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst ExternalLogins = {\n\tmixins: [settingsMixin, loadingMixin],\n\ttemplate: \"#ExternalLogins\"\n}\n\nconst RecentUsers = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#RecentUsers\",\n\tdata() {\n\t\treturn {\n\t\t\tresults: []\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.startLoad();\n\t\taxios.get(basePath + \"GetRecentUsers\").then(response => {\n\t\t\tthis.results = response.data;\n\t\t\tthis.endLoad();\n\t\t})\n\t\t\t.catch(error => this.errorAlert());\n\t},\n}\n\nconst EditUser = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#EditUser\",\n\tdata() {\n\t\treturn {\n\t\t\tsearchResults: [],\n\t\t\tsearchText: \"\",\n\t\t\tsearchType: \"name\",\n\t\t\tsearchAlert: false\n\t\t}\n\t},\n\tmethods: {\n\t\tsearch: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"EditUserSearch\", { searchType: this.searchType, searchText: this.searchText })\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.searchResults = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t\tif (this.searchResults.length === 0)\n\t\t\t\t\t\tthis.searchAlert = true;\n\t\t\t\t\telse\n\t\t\t\t\t\tthis.searchAlert = false;\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst EditUserDetail = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#EditUserDetail\",\n\tdata() {\n\t\treturn {\n\t\t\tuser: { newEmail: \"\", newPassword: \"\" },\n\t\t\troles: []\n\t\t}\n\t},\n\tcreated: function () {\n\t\tvar u = basePath + \"GetUser/\" + this.$route.params.id;\n\t\taxios.get(u).then(response => {\n\t\t\tthis.user = response.data;\n\t\t})\n\t\t\t.catch(error => this.errorAlert());\n\t\tthis.startLoad();\n\t\taxios.get(basePath + \"GetAllRoles\").then(response => {\n\t\t\tthis.roles = response.data;\n\t\t\tthis.endLoad();\n\t\t})\n\t\t\t.catch(error => this.errorAlert());\n\t},\n\tmethods: {\n\t\tsaveUser: function (message) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"SaveUser\", this.user).then(response => {\n\t\t\t\tthis.endLoad(message);\n\t\t\t}).catch(error => {\n\t\t\t\talert(error);\n\t\t\t});\n\t\t},\n\t\tuploadAvatar: function (target) {\n\t\t\tconst formData = new FormData();\n\t\t\tvar files = target.files;\n\t\t\tformData.append(\"avatarFile\", files[0], files[0].name);\n\t\t\taxios.post(basePath + \"UpdateUserAvatar/\" + this.user.userID, formData).then(response => {\n\t\t\t\tthis.user.avatarID = response.data.avatarID;\n\t\t\t\ttarget.value = \"\";\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tremoveAvatar: function () {\n\t\t\taxios.post(basePath + \"UpdateUserAvatar/\" + this.user.userID, null).then(response => {\n\t\t\t\tthis.user.avatarID = response.data.avatarID;\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tuploadImage: function (target) {\n\t\t\tconst formData = new FormData();\n\t\t\tvar files = target.files;\n\t\t\tformData.append(\"imageFile\", files[0], files[0].name);\n\t\t\taxios.post(basePath + \"UpdateUserImage/\" + this.user.userID, formData).then(response => {\n\t\t\t\tthis.user.imageID = response.data.imageID;\n\t\t\t\ttarget.value = \"\";\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tremoveImage: function () {\n\t\t\taxios.post(basePath + \"UpdateUserImage/\" + this.user.userID, null).then(response => {\n\t\t\t\tthis.user.imageID = response.data.imageID;\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n        },\n        deleteUser: function () {\n            axios.post(basePath + \"DeleteUser/\" + this.user.userID, null).then(response => {\n                this.$router.push(\"/edituser\");\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n        },\n        deleteAndBanUser: function () {\n            axios.post(basePath + \"DeleteAndBanUser/\" + this.user.userID, null).then(response => {\n                this.$router.push(\"/edituser\");\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n        }\n\t}\n}\n\nconst UserRoles = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#UserRoles\",\n\tdata() {\n\t\treturn {\n\t\t\troles: [],\n\t\t\tnewRole: \"\",\n\t\t\tselectedAll: null\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.refreshRoles();\n\t},\n\tmethods: {\n\t\trefreshRoles: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.get(basePath + \"GetAllRoles\").then(response => {\n\t\t\t\tthis.roles = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tcreateRole: function () {\n\t\t\tif (this.newRole && this.newRole != \"\") {\n\t\t\t\tthis.startLoad();\n\t\t\t\taxios.post(basePath + \"CreateRole/\" + this.newRole).then(response => {\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t\tthis.refreshRoles();\n\t\t\t\t\tthis.newRole = \"\";\n\t\t\t\t})\n\t\t\t\t\t.catch(error => this.errorAlert());\n\t\t\t}\n\t\t},\n\t\tdeleteRole: function () {\n\t\t\tif (this.selectedAll) {\n\t\t\t\tthis.startLoad();\n\t\t\t\taxios.post(basePath + \"DeleteRole/\" + this.selectedAll).then(response => {\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t\tthis.refreshRoles();\n\t\t\t\t})\n\t\t\t\t\t.catch(error => this.errorAlert());\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst UserImageApproval = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#UserImageApproval\",\n\tdata() {\n\t\treturn {\n\t\t\tisNewUserImageApproved: false,\n\t\t\tunapproved: []\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.refreshData();\n\t},\n\tmethods: {\n\t\trefreshData: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.get(basePath + \"GetImageApproval\").then(response => {\n\t\t\t\tthis.isNewUserImageApproved = response.data.isNewUserImageApproved;\n\t\t\t\tthis.unapproved = response.data.unapproved;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tapproveImage: function (id) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"ApproveUserImage/\" + id).then(response => {\n\t\t\t\tthis.refreshData();\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tdeleteImage: function (id) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"DeleteUserImage/\" + id).then(response => {\n\t\t\t\tthis.refreshData();\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst EmailIpBan = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#EmailIpBan\",\n\tdata() {\n\t\treturn {\n\t\t\tips: [],\n\t\t\temails: [],\n\t\t\tnewEmail: \"\",\n\t\t\tnewIP: \"\",\n\t\t\tselectedIP: null,\n\t\t\tselectedEmail: null\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.refreshData();\n\t},\n\tmethods: {\n\t\trefreshData: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.get(basePath + \"GetEmailIPBan\").then(response => {\n\t\t\t\tthis.emails = response.data.emails;\n\t\t\t\tthis.ips = response.data.ips;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tbanEmail: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"BanEmail\", { string: this.newEmail }).then(response => {\n\t\t\t\tthis.endLoad();\n\t\t\t\tthis.newEmail = \"\";\n\t\t\t\tthis.refreshData();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tremoveEmail: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"RemoveEmail\", { string: this.selectedEmail }).then(response => {\n\t\t\t\tthis.endLoad();\n\t\t\t\tthis.refreshData();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tbanIP: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"BanIP\", { string: this.newIP }).then(response => {\n\t\t\t\tthis.endLoad();\n\t\t\t\tthis.newIP = \"\";\n\t\t\t\tthis.refreshData();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tremoveIP: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"RemoveIP\", { string: this.selectedIP }).then(response => {\n\t\t\t\tthis.endLoad();\n\t\t\t\tthis.refreshData();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst EmailUsers = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#EmailUsers\",\n\tdata() {\n\t\treturn {\n\t\t\tsubject: \"\",\n\t\t\tbody: \"\",\n\t\t\thtmlBody: \"\",\n\t\t\terrorMessage: \"\",\n\t\t\tisErrorVisible: false,\n\t\t\tisSuccess: false\n\t\t}\n\t},\n\tmethods: {\n\t\tsendMail: function () {\n\t\t\tthis.isErrorVisible = false;\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"EmailUsers\", { subject: this.subject, body: this.body, htmlBody: this.htmlBody }).then(response => {\n\t\t\t\tthis.endLoad();\n\t\t\t\tthis.subject = \"\";\n\t\t\t\tthis.body = \"\";\n\t\t\t\tthis.htmlBody = \"\";\n\t\t\t\tthis.isSuccess = true;\n\t\t\t})\n\t\t\t\t.catch(e => {\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t\tthis.isErrorVisible = true;\n\t\t\t\t\tthis.isSuccess = false;\n\t\t\t\t\tthis.errorMessage = e.response.data.error;\n\t\t\t\t});\n\t\t}\n\t}\n}\n\nconst ScoringGame = {\n\tmixins: [settingsMixin, loadingMixin],\n\ttemplate: \"#ScoringGame\"\n}\n\nconst EventDefinitions = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#EventDefinitions\",\n\tdata() {\n\t\treturn {\n\t\t\tallEvents: [],\n\t\t\tstaticIDs: [],\n\t\t\tnewEvent: {\n\t\t\t\teventDefinitionID: \"\",\n\t\t\t\tdescription: \"\",\n\t\t\t\tpointValue: \"\",\n\t\t\t\tisPublishedToFeed: false\n\t\t\t}\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.refreshData();\n\t},\n\tmethods: {\n\t\trefreshData: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.get(basePath + \"GetAllEventDefinitions\").then(response => {\n\t\t\t\tthis.allEvents = response.data.allEvents;\n\t\t\t\tthis.staticIDs = response.data.staticIDs;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tdeleteEvent: function (event) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"DeleteEvent/\" + event.eventDefinitionID)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.refreshData();\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tresetEvent: function () {\n\t\t\tthis.newEvent.eventDefinitionID = \"\";\n\t\t\tthis.newEvent.description = \"\";\n\t\t\tthis.newEvent.pointValue = \"\";\n\t\t\tthis.newEvent.isPublishedToFeed = false;\n\t\t},\n\t\topenNewEvent: function () {\n\t\t\tthis.resetEvent();\n\t\t\tconst e = this.$refs.modal;\n\t\t\tvar modal = new bootstrap.Modal(e);\n\t\t\tmodal.show();\n\t\t},\n\t\tcreateEvent: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"CreateEvent\", this.newEvent)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.refreshData();\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst AwardDefinitions = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#AwardDefinitions\",\n\tdata() {\n\t\treturn {\n\t\t\tallAwards: [],\n\t\t\tnewAward: {\n\t\t\t\tawardDefinitionID: \"\",\n\t\t\t\ttitle: \"\",\n\t\t\t\tdescription: \"\",\n\t\t\t\tisSingleTimeAward: false\n\t\t\t}\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.refreshData();\n\t},\n\tmethods: {\n\t\trefreshData: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.get(basePath + \"GetAllAwardDefinitions\").then(response => {\n\t\t\t\tthis.allAwards = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tdeleteAward: function (award) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"DeleteAward/\" + award.awardDefinitionID)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.refreshData();\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tresetAward: function () {\n\t\t\tthis.newAward.awardDefinitionID = \"\";\n\t\t\tthis.newAward.title = \"\";\n\t\t\tthis.newAward.description = \"\";\n\t\t\tthis.newAward.isSingleTimeAward = false;\n\t\t},\n\t\topenNewAward: function () {\n\t\t\tthis.resetAward();\n\t\t\tconst e = this.$refs.modal;\n\t\t\tvar modal = new bootstrap.Modal(e);\n\t\t\tmodal.show();\n\t\t},\n\t\tcreateAward: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"CreateAward\", this.newAward)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.refreshData();\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst AwardDefinitionDetail = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#AwardDefinitionDetail\",\n\tdata() {\n\t\treturn {\n\t\t\taward: {},\n\t\t\tconditions: [],\n\t\t\tallEvents: [],\n\t\t\tnewCondition: {\n\t\t\t\tawardDefinitionID: 0,\n\t\t\t\teventDefinitionID: \"\",\n\t\t\t\teventCount: 0\n\t\t\t}\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.refreshData();\n\t},\n\tmethods: {\n\t\trefreshData: function () {\n\t\t\tthis.startLoad();\n\t\t\tvar u = basePath + \"GetAward/\" + this.$route.params.id;\n\t\t\taxios.get(u).then(response => {\n\t\t\t\tthis.award = response.data.award;\n\t\t\t\tthis.conditions = response.data.conditions;\n\t\t\t\tthis.allEvents = response.data.allEvents;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tdeleteCondition: function (c) {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"DeleteCondition\", { awardDefinitionID: this.award.awardDefinitionID, eventDefinitionID: c.eventDefinitionID })\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.refreshData();\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tcreateCondition: function () {\n\t\t\tthis.newCondition.awardDefinitionID = this.award.awardDefinitionID;\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"CreateCondition\", this.newCondition)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.refreshData();\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\topenNewCondition: function () {\n\t\t\tthis.newCondition.eventCount = \"\";\n\t\t\tconst e = this.$refs.modal;\n\t\t\tvar modal = new bootstrap.Modal(e);\n\t\t\tmodal.show();\n\t\t}\n\t}\n}\n\nconst ManualEvent = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#ManualEvent\",\n\tdata() {\n\t\treturn {\n\t\t\tallEvents: [],\n\t\t\tsearchName: \"\",\n\t\t\tsearchResults: [],\n\t\t\tselectedUser: {},\n\t\t\tmessage: \"\",\n\t\t\tpoints: 0,\n\t\t\teventDefinitionID: \"\"\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.startLoad();\n\t\taxios.get(basePath + \"GetAllEvents\")\n\t\t\t.then(response => {\n\t\t\t\tthis.allEvents = response.data;\n\t\t\t\tthis.endLoad();\n\t\t\t})\n\t\t\t.catch(error => this.errorAlert());\n\t},\n\tmethods: {\n\t\topenSearch: function () {\n\t\t\tconst e = this.$refs.modal;\n\t\t\tvar modal = new bootstrap.Modal(e);\n\t\t\tmodal.show();\n\t\t},\n\t\tupdateList: function () {\n\t\t\tif (this.searchName.length < 2) return;\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"GetNames\", { string: this.searchName })\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.searchResults = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tchooseUser: function () {\n\t\t},\n\t\tcreateManualEvent: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"CreateManualEvent\", { userID: this.selectedUser.userID, message: this.message, points: this.points })\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t\tthis.message = \"\";\n\t\t\t\t\tthis.points = 0;\n\t\t\t\t})\n\t\t\t\t.catch(error => {\n\t\t\t\t\talert(error.response.data);\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t});\n\t\t},\n\t\tcreateExistingManualEvent: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"CreateExistingManualEvent\", { userID: this.selectedUser.userID, message: this.message, points: null, eventDefinitionID: this.eventDefinitionID })\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t\tthis.message = \"\";\n\t\t\t\t})\n\t\t\t\t.catch(error => {\n\t\t\t\t\talert(error.response.data);\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t});\n\t\t}\n\t}\n}\n\nconst IPHistory = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#IPHistory\",\n\tdata() {\n\t\treturn {\n\t\t\thistory: [],\n\t\t\tquery: { iP: \"\", start: \"\", end: \"\" }\n\t\t}\n\t},\n\tmethods: {\n\t\tgetHistory: function () {\n\t\t\tthis.startLoad();\n\t\t\tvar copy = Object.assign({}, this.query);\n\t\t\tcopy.start = new Date(this.query.start).toISOString();\n\t\t\tcopy.end = new Date(this.query.end).toISOString();\n\t\t\taxios.post(basePath + \"QueryIPHistory\", copy)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.history = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst SecurityLog = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#SecurityLog\",\n\tdata() {\n\t\treturn {\n\t\t\thistory: [],\n\t\t\tquery: { searchTerm: \"\", type: \"Name\", start: \"\", end: \"\" }\n\t\t}\n\t},\n\tmethods: {\n\t\tgetHistory: function () {\n\t\t\tthis.startLoad();\n\t\t\tvar copy = Object.assign({}, this.query);\n\t\t\tcopy.start = new Date(this.query.start).toISOString();\n\t\t\tcopy.end = new Date(this.query.end).toISOString();\n\t\t\taxios.post(basePath + \"QuerySecurityLog\", copy)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.history = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst ModerationLog = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#ModerationLog\",\n\tdata() {\n\t\treturn {\n\t\t\thistory: [],\n\t\t\tquery: { start: \"\", end: \"\" }\n\t\t}\n\t},\n\tmethods: {\n\t\tgetHistory: function () {\n\t\t\tthis.startLoad();\n\t\t\tvar copy = Object.assign({}, this.query);\n\t\t\tcopy.start = new Date(this.query.start).toISOString();\n\t\t\tcopy.end = new Date(this.query.end).toISOString();\n\t\t\taxios.post(basePath + \"QueryModerationLog\", copy)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.history = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst ErrorLog = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#ErrorLog\",\n\tdata() {\n\t\treturn {\n\t\t\terrorList: { pageIndex: 1, pageSize: 20, list: [] }\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.getErrors();\n\t},\n\tmethods: {\n\t\tgetMore: function (newIndex) {\n\t\t\tthis.errorList.pageIndex = newIndex;\n\t\t\tthis.getErrors();\n\t\t},\n\t\tgetErrors: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.get(basePath + \"GetErrorLog/\" + this.errorList.pageIndex)\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.errorList = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tdeleteAll() {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"DeleteAllErrors\")\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.errorList.pageIndex = 1;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t\tthis.getErrors();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst Services = {\n\tmixins: [loadingMixin],\n\ttemplate: \"#Services\",\n\tdata() {\n\t\treturn {\n\t\t\tlist: {}\n\t\t}\n\t},\n\tcreated: function () {\n\t\tthis.getData();\n\t},\n\tmethods: {\n\t\tgetData: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.get(basePath + \"GetServices\")\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.list = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t},\n\t\tclearAll: function () {\n\t\t\tthis.startLoad();\n\t\t\taxios.post(basePath + \"ClearServices\")\n\t\t\t\t.then(response => {\n\t\t\t\t\tthis.list = response.data;\n\t\t\t\t\tthis.endLoad();\n\t\t\t\t})\n\t\t\t\t.catch(error => this.errorAlert());\n\t\t}\n\t}\n}\n\nconst routes = [\n\t{\n\t\tpath: \"/\", component: Top, redirect: \"/general\",\n\t\tchildren: [\n\t\t\t{ path: \"/general\", component: General },\n\t\t\t{ path: \"/categories\", component: Categories },\n\t\t\t{ path: \"/forums\", component: Forums },\n\t\t\t{ path: \"/forumpermissions\", component: ForumPermissions },\n\t\t\t{ path: \"/email\", component: Email },\n\t\t\t{ path: \"/search\", component: Search },\n\t\t\t{ path: \"/externallogins\", component: ExternalLogins },\n\t\t\t{ path: \"/recentusers\", component: RecentUsers },\n\t\t\t{ path: \"/edituser\", component: EditUser },\n\t\t\t{ path: \"/edituser/:id\", component: EditUserDetail },\n\t\t\t{ path: \"/userroles\", component: UserRoles },\n\t\t\t{ path: \"/userimageapproval\", component: UserImageApproval },\n\t\t\t{ path: \"/emailipban\", component: EmailIpBan },\n\t\t\t{ path: \"/emailusers\", component: EmailUsers },\n\t\t\t{ path: \"/scoringgame\", component: ScoringGame },\n\t\t\t{ path: \"/eventdefinitions\", component: EventDefinitions },\n\t\t\t{ path: \"/awarddefinitions\", component: AwardDefinitions },\n\t\t\t{ path: \"/awarddefinitions/:id\", component: AwardDefinitionDetail },\n\t\t\t{ path: \"/manualevent\", component: ManualEvent },\n\t\t\t{ path: \"/iphistory\", component: IPHistory },\n\t\t\t{ path: \"/securitylog\", component: SecurityLog },\n\t\t\t{ path: \"/moderationlog\", component: ModerationLog },\n\t\t\t{ path: \"/errorlog\", component: ErrorLog },\n\t\t\t{ path: \"/services\", component: Services }\n\t\t]\n\t},\n\t{ path: \"/:pathMatch(.*)*\", redirect: \"/general\" }\n];\n\nconst router = VueRouter.createRouter({\n\tcaseSensitive: false,\n\troutes: routes,\n\thistory: VueRouter.createWebHistory(\"/forums/admin\")\n});\n\nconst app = Vue.createApp({})\napp.use(router);\napp.mount(\"#app\");\n"
  },
  {
    "path": "src/PopForums.Mvc/wwwroot/Editor.css",
    "content": "﻿body {\n\tmargin: 10px;\n}\nblockquote {\n\tfont-size: 100%;\n\tborder-left: 5px solid #aaaaaa;\n\tpadding: 0 1em;\n}\n\nimg {\n\tmax-width: 100%;\n}"
  },
  {
    "path": "src/PopForums.Mvc/wwwroot/PopForums.css",
    "content": "﻿#ForumContainer h1 .icon {\n\tfont-size: .8em;\n}\n\n#ForumContainer .navbar {\n\tz-index: 1025;\n}\n\n#ForumContainer #PostResponseMessage, #ForumContainer #TopicModerationLog {\n\tdisplay: none;\n}\n\n#ForumContainer #NotificationList .card-footer {\n    font-size: .8em;\n}\n\n#ForumContainer .toLabelX {\n\tcursor: pointer;\n}\n\n#ForumContainer #ToModal #ToResultList {\n\theight: 200px;\n\toverflow-y: scroll;\n\tmargin-top: 10px;\n}\n\n#ForumContainer #PMToBox div {\n\tmargin-right: 5px;\n}\n\n#ForumContainer .answerData {\n\tmin-width: 4em;\n}\n\n#ForumContainer .answerButton, \npf-notificationtoggle,\n#ForumContainer .postUserData h3 {\n\tcursor: pointer;\n}\n\n#ForumContainer .QAstate {\n\twidth: 3em;\n}\n\n#ForumContainer .newPostBlock {\n\tborder-left: solid 8px;\n\tborder-color: var(--bs-secondary);\n\tpadding-left: 10px;\n}\n\n/* post item */\n\n#ForumContainer blockquote {\n\tfont-size: 100%;\n\tborder-left: 8px solid var(--bs-secondary);\n\tpadding: 0 1em;\n}\n#ForumContainer .miniProfileBox {\n\theight: 0;\n\toverflow: hidden;\n\ttransition: all .5s ease-in-out;\n}\n.voters {\n    max-height: 10em;\n    overflow: auto;\n}\n.voters ul { padding: 0; margin: 0; }\n.voters li { list-style: none;font-size: 100%; }\npf-votecount div span {\n    font-size: 1.2em;\n    margin-right: 1em;\n    cursor: pointer;\n}\n\n#ForumContainer .voteUpContainer {\n\twidth: 6.5em;\n}\n#ForumContainer .voteUpContainerVert {\n\theight: 6.5em;\n}\n\n#ForumContainer .postToolContainer {\n    clear: both;\n}\n#ForumContainer .postToolContainer .toolButton {\n    width: 2em;\n    text-align: center;\n\tfont-size: 1.3em;\n    margin: auto 0;\n}\n#ForumContainer .postToolContainer a, #ForumContainer .postToolContainer button {\n\tcolor: var(--bs-body-color);\n}\n#ForumContainer .postToolContainer *, #ForumContainer h2 a {\n\ttext-decoration: none;\n    cursor: pointer;\n\tvertical-align: middle;\n}\n#ForumContainer .postItem .postImage {\n\tmax-width: 100%;height: auto;\n}\n#ForumContainer .postItem iframe {\n\tmax-width: 100%;\n}\n#ForumContainer .postTime {\n\tdisplay: block;\n}\n\n/* text editor */\n\n#ForumContainer #FullText {\n\theight: 300px;\n}\n#tinymce {\n\tpadding: 10px !important;\n}\n#ForumContainer .postForm {\n\tmargin: 10px 0;\n}\n\n#ForumContainer #ParsedFullText {\n\toverflow-y: scroll;overflow-x: hidden;\n\tmax-height: 350px;\n}\n\n#ForumContainer .voteCount .voters {\n\tdisplay: none;\n}\n\n#ForumContainer .morePostsButton {\n\tmargin-bottom: 10px;\n}\n\n#ForumContainer .morePager:hover, #ForumContainer .currentPager:hover {\n\tbackground: none;\n}\n#ForumContainer .morePager, #ForumContainer .currentPager .page-link {\n\tcursor: default;\n\twhite-space: nowrap;\n}\n\n#ForumContainer .modal-body img {\n\tmax-width: 100%;\n}\n\n/* queries */\n\n@media(min-width:1px) {\n\t#ForumContainer #PreviewModal .modal-dialog {\n\t\twidth: 90%!important;\n\t\tmax-height: 95% !important;\n\t\tmargin: 10px auto;\n\t}\n}\n\n/* Bootstrap overrides */\n\n#ForumContainer .navbar .badge {\n\tbackground-color: var(--bs-danger);\n\tcolor: var(--bs-white);\n\tposition: relative;\n\ttop: -2px;\n}\n\n#ForumContainer .explode {\n\tanimation: explode 0.5s;\n\tanimation-fill-mode: forwards;\n}\n@keyframes explode {\n\t45% {\n\t\ttransform: scale(2, 2);\n\t}\n\t55% {\n\t\ttransform: scale(2, 2);\n\t}\n\t50% {\n\t\tbackground-color: var(--bs-light);\n\t}\n}\n\n#ForumContainer .hidden {\n\tdisplay: none !important;\n}\n\n#ForumContainer .breadcrumb {\n\tpadding: .75em;\n\tbackground-color: var(--bs-light);\n}\n#ForumContainer .breadcrumb a {\n\ttext-decoration: none;\n}\n\n.btn-link {\n\tborder: none;\n\tcursor: pointer;\n\tbackground: none;\n}\n\n/* icons */\n\n.miniProfile a.icon {\n\ttext-decoration: none !important;\n\tmargin-right: .5em;\n}\n\n\n:root {\n\t--bs-light-rgb: 240, 240, 240;\n\t--bs-light: #f0f0f0;\n}\n.alert-light {\n    --bs-alert-bg: #f0f0f0;\n}\n\n#AccountBox .icon, #SearchDropDown .icon {\n\tfont-size: 1.3em;\n\tvertical-align: text-top;\n    margin-left: 0.2em;\n}\n\n@font-face {\n\tfont-family: 'icomoon';\n\tsrc: url('Fonts/icomoon.ttf?rv0tpv') format('truetype'), url('Fonts/icomoon.woff?rv0tpv') format('woff'), url('Fonts/icomoon.svg?rv0tpv#icomoon') format('svg');\n\tfont-weight: normal;\n\tfont-style: normal;\n\tfont-display: block;\n}\n\n.icon {\n\t/* use !important to prevent issues with browser extensions that change fonts */\n\tfont-family: 'icomoon' !important;\n\tspeak: none;\n\tfont-style: normal;\n\tfont-weight: normal;\n\tfont-variant: normal;\n\ttext-transform: none;\n\tline-height: 1;\n\t/* Better Font Rendering =========== */\n\t-webkit-font-smoothing: antialiased;\n\t-moz-osx-font-smoothing: grayscale;\n}\n\n.icon-recycle:before {\n    content: \"\\e94c\";\n}\n\n.icon-trophy:before {\n\tcontent: \"\\e900\";\n}\n\n.icon-trophy-fill:before {\n\tcontent: \"\\e901\";\n}\n\n.icon-bell-fill:before {\n\tcontent: \"\\e902\";\n}\n\n.icon-bell:before {\n\tcontent: \"\\e903\";\n}\n\n.icon-bell-slash:before {\n\tcontent: \"\\e904\";\n}\n\n.icon-card-list:before {\n\tcontent: \"\\e905\";\n}\n\n.icon-quote:before {\n\tcontent: \"\\e907\";\n}\n\n.icon-link:before {\n\tcontent: \"\\e908\";\n}\n\n.icon-arrow-counterclockwise:before {\n\tcontent: \"\\e909\";\n}\n\n.icon-arrow-down-circle:before {\n\tcontent: \"\\e90a\";\n}\n\n.icon-arrow-left-circle:before {\n\tcontent: \"\\e90b\";\n}\n\n.icon-arrow-right-circle:before {\n\tcontent: \"\\e90c\";\n}\n\n.icon-arrow-up-circle:before {\n\tcontent: \"\\e90d\";\n}\n\n.icon-box-arrow-up-right:before {\n\tcontent: \"\\e90e\";\n}\n\n.icon-chat:before {\n\tcontent: \"\\e90f\";\n}\n\n.icon-chat-dots:before {\n\tcontent: \"\\e910\";\n}\n\n.icon-check-circle:before {\n\tcontent: \"\\e911\";\n}\n\n.icon-check-circle-fill:before {\n\tcontent: \"\\e912\";\n}\n\n.icon-clock-history:before {\n\tcontent: \"\\e913\";\n}\n\n.icon-cloud-arrow-up:before {\n\tcontent: \"\\e914\";\n}\n\n.icon-cloud-arrow-up-fill:before {\n\tcontent: \"\\e915\";\n}\n\n.icon-dash-circle:before {\n\tcontent: \"\\e916\";\n}\n\n.icon-envelope:before {\n\tcontent: \"\\e917\";\n}\n\n.icon-exclamation-circle-fill:before {\n\tcontent: \"\\e918\";\n}\n\n.icon-exclamation-octagon-fill:before {\n\tcontent: \"\\e919\";\n}\n\n.icon-eye:before {\n\tcontent: \"\\e91a\";\n}\n\n.icon-eye-fill:before {\n\tcontent: \"\\e91b\";\n}\n\n.icon-facebook:before {\n\tcontent: \"\\e91c\";\n}\n\n.icon-file-earmark-text:before {\n\tcontent: \"\\e91d\";\n}\n\n.icon-file-earmark-text-fill:before {\n\tcontent: \"\\e91e\";\n}\n\n.icon-file-text:before {\n\tcontent: \"\\e91f\";\n}\n\n.icon-file-text-fill:before {\n\tcontent: \"\\e920\";\n}\n\n.icon-gear-fill:before {\n\tcontent: \"\\e921\";\n}\n\n.icon-google:before {\n\tcontent: \"\\e922\";\n}\n\n.icon-hand-thumbs-down:before {\n\tcontent: \"\\e923\";\n}\n\n.icon-hand-thumbs-down-fill:before {\n\tcontent: \"\\e924\";\n}\n\n.icon-hand-thumbs-up:before {\n\tcontent: \"\\e925\";\n}\n\n.icon-hand-thumbs-up-fill:before {\n\tcontent: \"\\e926\";\n}\n\n.icon-heart:before {\n\tcontent: \"\\e927\";\n}\n\n.icon-heart-fill:before {\n\tcontent: \"\\e928\";\n}\n\n.icon-house-door:before {\n\tcontent: \"\\e929\";\n}\n\n.icon-image:before {\n\tcontent: \"\\e92a\";\n}\n\n.icon-images:before {\n\tcontent: \"\\e92b\";\n}\n\n.icon-info-circle:before {\n\tcontent: \"\\e92c\";\n}\n\n.icon-info-circle-fill:before {\n\tcontent: \"\\e92d\";\n}\n\n.icon-instagram:before {\n\tcontent: \"\\e92e\";\n}\n\n.icon-lock:before {\n\tcontent: \"\\e92f\";\n}\n\n.icon-lock-fill:before {\n\tcontent: \"\\e930\";\n}\n\n.icon-microsoft:before {\n\tcontent: \"\\e931\";\n}\n\n.icon-paperclip:before {\n\tcontent: \"\\e932\";\n}\n\n.icon-pencil-square:before {\n\tcontent: \"\\e933\";\n}\n\n.icon-people:before {\n\tcontent: \"\\e934\";\n}\n\n.icon-people-fill:before {\n\tcontent: \"\\e935\";\n}\n\n.icon-person:before {\n\tcontent: \"\\e936\";\n}\n\n.icon-person-fill:before {\n\tcontent: \"\\e937\";\n}\n\n.icon-pin-angle:before {\n\tcontent: \"\\e938\";\n}\n\n.icon-pin-angle-fill:before {\n\tcontent: \"\\e939\";\n}\n\n.icon-plus-square:before {\n\tcontent: \"\\e93a\";\n}\n\n.icon-plus-square-fill:before {\n\tcontent: \"\\e93b\";\n}\n\n.icon-question-circle:before {\n\tcontent: \"\\e93c\";\n}\n\n.icon-question-circle-fill:before {\n\tcontent: \"\\e93d\";\n}\n\n.icon-reply:before {\n\tcontent: \"\\e93e\";\n}\n\n.icon-reply-fill:before {\n\tcontent: \"\\e93f\";\n}\n\n.icon-rss-fill:before {\n\tcontent: \"\\e940\";\n}\n\n.icon-search:before {\n\tcontent: \"\\e941\";\n}\n\n.icon-share-fill:before {\n\tcontent: \"\\e942\";\n}\n\n.icon-skip-backward-fill:before {\n\tcontent: \"\\e943\";\n}\n\n.icon-skip-end-fill:before {\n\tcontent: \"\\e944\";\n}\n\n.icon-skip-forward-fill:before {\n\tcontent: \"\\e945\";\n}\n\n.icon-skip-start-fill:before {\n\tcontent: \"\\e946\";\n}\n\n.icon-star:before {\n\tcontent: \"\\e947\";\n}\n\n.icon-star-fill:before {\n\tcontent: \"\\e948\";\n}\n\n.icon-trash3-fill:before {\n\tcontent: \"\\e949\";\n}\n\n.icon-unlock-fill:before {\n\tcontent: \"\\e94a\";\n}\n\n.icon-youtube:before {\n\tcontent: \"\\e94b\";\n}\n\n\n\n\n\n\n\n\n\n\n\n#ForumContainer .newIndicator a {\n\ttext-decoration: none;\n}\n#ForumContainer .topicIndicator {\n\tfont-size: 2em;\n\twidth: 40px;\n}\n\n#ForumContainer .topicIndicator .topicIndicatorBadge {\n\tposition: relative;\n\tfont-size: 60%;\n\ttext-shadow: -2px 0 white, 0 2px white, 2px 0 white, 0 -2px white;\n}\n#ForumContainer .topicIndicator .soloLeftBadge {\n\tleft: -95%;\n}\n#ForumContainer .topicIndicator .firstBadge {\n\tleft: -25%;\n}\n#ForumContainer .topicIndicator .secondBadge {\n\tleft: -145%;\n}\n\n#ForumContainer #Profile div.row div {\n\tpadding:.5em;\n}"
  },
  {
    "path": "src/PopForums.Sql/CacheHelper.cs",
    "content": "﻿namespace PopForums.Sql;\n\npublic class CacheHelper : ICacheHelper\n{\n\tpublic CacheHelper(IConfig config, ITenantService tenantService)\n\t{\n\t\t_config = config;\n\t\t_tenantService = tenantService;\n\t\tif (_cache == null)\n\t\t{\n\t\t\tvar options = new MemoryCacheOptions();\n\t\t\t_cache = new MemoryCache(options);\n\t\t}\n\t}\n\n\tprivate readonly IConfig _config;\n\tprivate readonly ITenantService _tenantService;\n\tprivate static IMemoryCache _cache;\n\n\tprivate string PrefixTenantOnKey(string key)\n\t{\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\treturn $\"{tenantID}:{key}\";\n\t}\n\n\tpublic void SetCacheObject(string key, object value)\n\t{\n\t\tkey = PrefixTenantOnKey(key);\n\t\tvar options = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_config.CacheSeconds) };\n\t\t_cache.Set(key, value, options);\n\t}\n\n\tpublic void SetCacheObject(string key, object value, double seconds)\n\t{\n\t\tkey = PrefixTenantOnKey(key);\n\t\tvar options = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(seconds) };\n\t\t_cache.Set(key, value, options);\n\t}\n\n\tpublic void SetLongTermCacheObject(string key, object value)\n\t{\n\t\tkey = PrefixTenantOnKey(key);\n\t\tvar options = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(60) };\n\t\t_cache.Set(key, value, options);\n\t}\n\n\tpublic void SetPagedListCacheObject<T>(string rootKey, int page, List<T> value)\n\t{\n\t\trootKey = PrefixTenantOnKey(rootKey);\n\t\t_cache.TryGetValue(rootKey, out Dictionary<int, List<T>> rootPages);\n\t\tif (rootPages == null)\n\t\t\trootPages = new Dictionary<int, List<T>>();\n\t\telse if (rootPages.ContainsKey(page))\n\t\t\trootPages.Remove(page);\n\t\trootPages.Add(page, value);\n\t\tvar options = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_config.CacheSeconds) };\n\t\t_cache.Set(rootKey, rootPages, options);\n\t}\n\n\tpublic void RemoveCacheObject(string key)\n\t{\n\t\tkey = PrefixTenantOnKey(key);\n\t\t_cache.Remove(key);\n\t\tOnRemoveCacheKey?.Invoke(key);\n\t}\n\n\tpublic T GetCacheObject<T>(string key)\n\t{\n\t\tkey = PrefixTenantOnKey(key);\n\t\tvar cacheObject = _cache.Get(key);\n\t\treturn cacheObject != null ? (T)cacheObject : default;\n\t}\n\n\tpublic List<T> GetPagedListCacheObject<T>(string rootKey, int page)\n\t{\n\t\trootKey = PrefixTenantOnKey(rootKey);\n\t\t_cache.TryGetValue(rootKey, out Dictionary<int, List<T>> rootPages);\n\t\tif (rootPages == null)\n\t\t\treturn null;\n\t\tif (rootPages.ContainsKey(page))\n\t\t\treturn rootPages[page];\n\t\treturn null;\n\t}\n\n\tpublic event Action<string> OnRemoveCacheKey;\n\t\n\tpublic string GetEffectiveCacheKey(string key)\n\t{\n\t\tvar effectiveKey = PrefixTenantOnKey(key);\n\t\treturn effectiveKey;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Extensions.cs",
    "content": "﻿namespace PopForums.Sql;\n\npublic static class Extensions\n{\n\tpublic static DbCommand Command(this DbConnection connection, ISqlObjectFactory sqlObjectFactory, string sql)\n\t{\n\t\tvar command = sqlObjectFactory.GetCommand(sql, connection);\n\t\treturn command;\n\t}\n\n\tpublic static DbCommand AddParameter(this DbCommand command, ISqlObjectFactory sqlObjectFactory, string parameterName, object value)\n\t{\n\t\tvar parameter = sqlObjectFactory.GetParameter(parameterName, value);\n\t\tcommand.Parameters.Add(parameter);\n\t\treturn command;\n\t}\n\n\tpublic static void Using(this DbConnection connection, Action<DbConnection> action)\n\t{\n\t\tusing (connection)\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tconnection.Open();\n\t\t\t\taction(connection);\n\t\t\t}\n\t\t\tfinally\n\t\t\t{\n\t\t\t\tconnection.Close();\n\t\t\t\tconnection.Dispose();\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic static async Task UsingAsync(this DbConnection connection, Func<DbConnection, Task> action)\n\t{\n\t\tawait using (connection)\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tawait connection.OpenAsync();\n\t\t\t\tawait action(connection);\n\t\t\t}\n\t\t\tfinally\n\t\t\t{\n\t\t\t\tawait connection.CloseAsync();\n\t\t\t\tconnection.Dispose();\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic static void AddPopForumsSql(this IServiceCollection services)\n\t{\n\t\tservices.AddTransient<ICacheHelper, CacheHelper>();\n\t\tservices.AddTransient<ISqlObjectFactory, SqlObjectFactory>();\n\t\tservices.AddTransient<IAwardCalculationQueueRepository, AwardCalculationQueueRepository>();\n\t\tservices.AddTransient<IAwardConditionRepository, AwardConditionRepository>();\n\t\tservices.AddTransient<IAwardDefinitionRepository, AwardDefinitionRepository>();\n\t\tservices.AddTransient<IBanRepository, BanRepository>();\n\t\tservices.AddTransient<ICategoryRepository, CategoryRepository>();\n\t\tservices.AddTransient<IEmailQueueRepository, EmailQueueRepository>();\n\t\tservices.AddTransient<IErrorLogRepository, ErrorLogRepository>();\n\t\tservices.AddTransient<IEventDefinitionRepository, EventDefinitionRepository>();\n\t\tservices.AddTransient<IExternalUserAssociationRepository, ExternalUserAssociationRepository>();\n\t\tservices.AddTransient<IFavoriteTopicsRepository, FavoriteTopicsRepository>();\n\t\tservices.AddTransient<IFeedRepository, FeedRepository>();\n\t\tservices.AddTransient<IForumRepository, ForumRepository>();\n\t\tservices.AddTransient<ILastReadRepository, LastReadRepository>();\n\t\tservices.AddTransient<IModerationLogRepository, ModerationLogRepository>();\n\t\tservices.AddTransient<IPointLedgerRepository, PointLedgerRepository>();\n\t\tservices.AddTransient<IPostRepository, PostRepository>();\n\t\tservices.AddTransient<IPrivateMessageRepository, PrivateMessageRepository>();\n\t\tservices.AddTransient<IProfileRepository, ProfileRepository>();\n\t\tservices.AddTransient<IQueuedEmailMessageRepository, QueuedEmailMessageRepository>();\n\t\tservices.AddTransient<IRoleRepository, RoleRepository>();\n\t\tservices.AddTransient<ISearchIndexQueueRepository, SearchIndexQueueRepository>();\n\t\tservices.AddTransient<ISearchRepository, SearchRepository>();\n\t\tservices.AddTransient<ISecurityLogRepository, SecurityLogRepository>();\n\t\tservices.AddTransient<ISettingsRepository, SettingsRepository>();\n\t\tservices.AddTransient<ISetupRepository, SetupRepository>();\n\t\tservices.AddTransient<ISubscribedTopicsRepository, SubscribedTopicsRepository>();\n\t\tservices.AddTransient<ITopicRepository, TopicRepository>();\n\t\tservices.AddTransient<ITopicViewLogRepository, TopicViewLogRepository>();\n\t\tservices.AddTransient<IUserAvatarRepository, UserAvatarRepository>();\n\t\tservices.AddTransient<IUserAwardRepository, UserAwardRepository>();\n\t\tservices.AddTransient<IUserImageRepository, UserImageRepository>();\n\t\tservices.AddTransient<IUserRepository, UserRepository>();\n\t\tservices.AddTransient<IUserSessionRepository, UserSessionRepository>();\n\t\tservices.AddTransient<IServiceHeartbeatRepository, ServiceHeartbeatRepository>();\n\t\tservices.AddTransient<INotificationRepository, NotificationRepository>();\n\t\tservices.AddTransient<IPostImageRepository, PostImageRepository>();\n\t\tservices.AddTransient<IPostImageTempRepository, PostImageTempRepository>();\n\t\tservices.AddTransient<ISubscribeNotificationRepository, SubscribeNotificationRepository>();\n\t\tservices.AddTransient<IIgnoreRepository, IgnoreRepository>();\n\t}\n\n\tpublic static object GetObjectOrDbNull(this object value)\n\t{\n\t\treturn value ?? DBNull.Value;\n\t}\n\n\tpublic static int? NullIntDbHelper(this DbDataReader reader, int index)\n\t{\n\t\tif (reader.IsDBNull(index)) return null;\n\t\treturn reader.GetInt32(index);\n\t}\n\n\tpublic static string NullStringDbHelper(this DbDataReader reader, int index)\n\t{\n\t\tif (reader.IsDBNull(index)) return null;\n\t\treturn reader.GetString(index);\n\t}\n\n\tpublic static Guid? NullGuidDbHelper(this IDataReader reader, int index)\n\t{\n\t\tif (reader.IsDBNull(index)) return null;\n\t\treturn reader.GetGuid(index);\n\t}\n\n\tpublic static string NullToEmpty(this string text)\n\t{\n\t\tif (String.IsNullOrEmpty(text))\n\t\t\treturn String.Empty;\n\t\treturn text;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Global.cs",
    "content": "﻿global using System;\nglobal using System.Collections.Generic;\nglobal using System.Data;\nglobal using System.Data.Common;\nglobal using System.IO;\nglobal using System.Linq;\nglobal using System.Reflection;\nglobal using System.Text;\nglobal using System.Text.RegularExpressions;\nglobal using System.Text.Json;\nglobal using System.Threading.Tasks;\n\nglobal using Microsoft.Data.SqlClient;\nglobal using Microsoft.Extensions.Caching.Memory;\nglobal using Microsoft.Extensions.DependencyInjection;\n\nglobal using Dapper;\n\nglobal using PopForums.Configuration;\nglobal using PopForums.Email;\nglobal using PopForums.ExternalLogin;\nglobal using PopForums.Models;\nglobal using PopForums.Repositories;\nglobal using PopForums.ScoringGame;\nglobal using PopForums.Services;\n\nglobal using PopForums.Sql.Repositories;"
  },
  {
    "path": "src/PopForums.Sql/ISqlObjectFactory.cs",
    "content": "﻿namespace PopForums.Sql;\n\npublic interface ISqlObjectFactory\n{\n\tDbConnection GetConnection();\n\tDbCommand GetCommand();\n\tDbCommand GetCommand(string sql);\n\tDbCommand GetCommand(string sql, DbConnection connection);\n\tDbParameter GetParameter(string parameterName, object value);\n}"
  },
  {
    "path": "src/PopForums.Sql/JsonElementTypeHandler.cs",
    "content": "﻿namespace PopForums.Sql;\n\npublic class JsonElementTypeHandler : SqlMapper.TypeHandler<JsonElement>\n{\n\tpublic override void SetValue(IDbDataParameter parameter, JsonElement value)\n\t{\n\t\tparameter.DbType = DbType.String;\n\t\tparameter.Size = int.MaxValue;\n\t\tparameter.Value = value.ToString();\n\t}\n\n\tpublic override JsonElement Parse(object value)\n\t{\n\t\tvar o = JsonSerializer.Deserialize<dynamic>((string)value);\n\t\tvar element = JsonSerializer.SerializeToElement(o, new JsonSerializerOptions {PropertyNamingPolicy = JsonNamingPolicy.CamelCase});\n\t\treturn element;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/PopForums.Sql.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<Description>PopForums.Data.Sql Class Library</Description>\n\t\t<VersionPrefix>22.0.0</VersionPrefix>\n\t\t<Authors>Jeff Putz</Authors>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<AssemblyName>PopForums.Sql</AssemblyName>\n\t\t<PackageId>PopForums.Sql</PackageId>\n\t\t<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>\n\t\t<PackageProjectUrl>https://github.com/POPWorldMedia/POPForums</PackageProjectUrl>\n\t\t<RepositoryUrl>https://github.com/POPWorldMedia/POPForums</RepositoryUrl>\n\t\t<Copyright>2025, POP World Media, LLC</Copyright>\n\t\t<PackageLicenseExpression>MIT</PackageLicenseExpression>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<EmbeddedResource Include=\"PopForums.sql\" Exclude=\"bin\\**;obj\\**;**\\*.xproj;packages\\**;@(EmbeddedResource)\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\PopForums\\PopForums.csproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Dapper\" Version=\"2.1.72\" />\n\t\t<PackageReference Include=\"Microsoft.Data.SqlClient\" Version=\"7.0.0\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"10.0.7\" />\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/PopForums.Sql/PopForums.sql",
    "content": "﻿-- ******************************************************** pf_PopForumsUser\n\nCREATE TABLE [dbo].[pf_PopForumsUser](\n\t[UserID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,\n\t[Name] [nvarchar](256) NOT NULL,\n\t[Email] [nvarchar](256) NOT NULL,\n\t[CreationDate] [datetime] NOT NULL,\n\t[IsApproved] [bit] NOT NULL DEFAULT ((-1)),\n\t[Password] [nvarchar](256) NOT NULL,\n\t[AuthorizationKey] [uniqueidentifier] NOT NULL DEFAULT ('00000000-0000-0000-0000-000000000000'),\n\t[Salt] [uniqueidentifier] NULL,\n    [TokenExpiration] [datetime] NULL\n);\n\nCREATE UNIQUE NONCLUSTERED INDEX [IX_PopForumsUser_UserName] ON [dbo].[pf_PopForumsUser]([Name]);\nCREATE UNIQUE NONCLUSTERED INDEX [IX_PopForumsUser_Email] ON [dbo].[pf_PopForumsUser]([Email]);\n\n\n\n\n-- ******************************************************** pf_Profile\n\nCREATE TABLE [dbo].[pf_Profile](\n\t[UserID] [int] NOT NULL PRIMARY KEY,\n\t[IsSubscribed] [bit] NOT NULL DEFAULT ((0)),\n\t[Signature] nvarchar(MAX) NOT NULL,\n\t[ShowDetails] [bit] NOT NULL DEFAULT ((0)),\n\t[Location] [nvarchar](256) NOT NULL,\n\t[IsPlainText] [bit] NOT NULL DEFAULT ((0)),\n\t[DOB] [datetime] NULL,\n\t[Web] [nvarchar](256) NOT NULL,\n\t[Facebook] [nvarchar](256) NULL,\n\t[Instagram] [nvarchar](256) NULL,\n\t[IsTos] [bit] NOT NULL DEFAULT ((0)),\n\t[AvatarID] [int] NULL,\n\t[ImageID] [int] NULL,\n\t[HideVanity] [bit] NOT NULL DEFAULT ((0)),\n\t[LastPostID] [int] NULL,\n\t[Points] [int] NOT NULL DEFAULT (0),\n\t[IsAutoFollowOnReply] [bit] NOT NULL DEFAULT(1)\n);\n\n\nALTER TABLE [dbo].[pf_Profile]  WITH CHECK ADD  CONSTRAINT [FK_pf_Profile_pf_PopForumsUser] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_Profile] CHECK CONSTRAINT [FK_pf_Profile_pf_PopForumsUser];\n\n\n-- ******************************************************** pf_UserActivity\n\nCREATE TABLE [dbo].[pf_UserActivity](\n\t[UserID] [int] NOT NULL PRIMARY KEY CLUSTERED,\n\t[LastActivityDate] [datetime] NOT NULL,\n\t[LastLoginDate] [datetime] NOT NULL,\n    [RefreshToken] [nvarchar](MAX) NULL\n);\n\nALTER TABLE [dbo].[pf_UserActivity]  WITH CHECK ADD  CONSTRAINT [FK_pf_UserActivity_pf_PopForumsUser] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_UserActivity] CHECK CONSTRAINT [FK_pf_UserActivity_pf_PopForumsUser];\n\n\n\n\n\n\n\n\n-- ******************************************************** pf_EmailBan\n\nCREATE TABLE [dbo].[pf_EmailBan] (\n\t[EmailBan] [nvarchar] (256) NOT NULL PRIMARY KEY \n);\n\n\n\n\n-- ******************************************************** pf_IPBan\n\nCREATE TABLE [dbo].[pf_IPBan] (\n\t[IPBan] [nvarchar] (256) NOT NULL PRIMARY KEY \n);\n\n\n\n\n\n-- ******************************************************** pf_Role and pf_PopForumsUserRole\n\nCREATE TABLE [dbo].[pf_Role] (\n\t[Role] [nvarchar] (256) NOT NULL PRIMARY KEY\n);\n\n\nCREATE TABLE [dbo].[pf_PopForumsUserRole] (\n\t[UserID] [int] NOT NULL ,\n\t[Role] [nvarchar] (256) NOT NULL\n);\n\n\nCREATE CLUSTERED INDEX [IX_PopForumsUserRole_UserID] ON [dbo].[pf_PopForumsUserRole]([UserID]);\n\n\nALTER TABLE [dbo].[pf_PopForumsUserRole]  WITH CHECK ADD  CONSTRAINT [FK_pf_PopForumsUserRole_Role] FOREIGN KEY([Role])\nREFERENCES [dbo].[pf_Role] ([Role])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_PopForumsUserRole] CHECK CONSTRAINT [FK_pf_PopForumsUserRole_Role];\n\n\nALTER TABLE [dbo].[pf_PopForumsUserRole]  WITH CHECK ADD  CONSTRAINT [FK_pf_PopForumsUserRole_UserID] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_PopForumsUserRole] CHECK CONSTRAINT [FK_pf_PopForumsUserRole_UserID];\n\n\nINSERT INTO pf_Role (Role) VALUES ('Admin');\n\nINSERT INTO pf_Role (Role) VALUES ('Moderator');\n\n\n-- ******************************************************** pf_SecurityLog\n\nCREATE TABLE [dbo].[pf_SecurityLog](\n\t[SecurityLogID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,\n\t[SecurityLogType] [int] NOT NULL,\n\t[UserID] [int] NULL,\n\t[TargetUserID] [int] NULL,\n\t[IP] [nvarchar](40) NOT NULL,\n\t[Message] [nvarchar](256) NOT NULL,\n\t[ActivityDate] [datetime] NOT NULL\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_SecurityLog_IP_ActivityDate] ON [dbo].[pf_SecurityLog] \n(\n\t[IP] ASC, [ActivityDate] DESC\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_SecurityLog_UserID_ActivityDate] ON [dbo].[pf_SecurityLog] \n(\n\t[UserID] ASC, [ActivityDate] DESC\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_SecurityLog_TargetUserID_ActivityDate] ON [dbo].[pf_SecurityLog] \n(\n\t[TargetUserID] ASC, [ActivityDate] DESC\n);\nCREATE NONCLUSTERED INDEX IX_pf_SecurityLog_TargetUserID_SecurityLogType ON pf_SecurityLog\n(\n\tTargetUserID DESC, SecurityLogType\n);\n\n\n\n\n-- ******************************************************** pf_Category\n\nCREATE TABLE [dbo].[pf_Category](\n\t[CategoryID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,\n\t[Title] [nvarchar](256) NOT NULL,\n\t[SortOrder] [int] NOT NULL\n);\n\n\n\n\n-- ******************************************************** pf_Forum\n\nCREATE TABLE [dbo].[pf_Forum](\n\t[ForumID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,\n\t[CategoryID] [int] NULL,\n\t[Title] [nvarchar](256) NOT NULL,\n\t[Description] [nvarchar](256) NOT NULL,\n\t[IsVisible] [bit] NOT NULL,\n\t[IsArchived] [bit] NOT NULL,\n\t[SortOrder] [int] NOT NULL,\n\t[TopicCount] [int] NOT NULL,\n\t[PostCount] [int] NOT NULL,\n\t[LastPostTime] [datetime] NOT NULL,\n\t[LastPostName] [nvarchar](256) NOT NULL,\n\t[UrlName] [nvarchar](256) NOT NULL,\n\t[ForumAdapterName] [varchar](256) NULL,\n\t[IsQAForum] [bit] NOT NULL DEFAULT ((0))\n);\n\n\nCREATE UNIQUE NONCLUSTERED INDEX [IX_pf_Forum_UrlName] ON [dbo].[pf_Forum] \n(\n\t[UrlName] ASC\n); \n\n\n\n\n\n\n-- ******************************************************** pf_Topic\n\nCREATE TABLE [dbo].[pf_Topic](\n\t[TopicID] [int] IDENTITY(1,1) NOT NULL,\n\t[ForumID] [int] NOT NULL DEFAULT (0),\n\t[Title] [nvarchar](256) NOT NULL,\n\t[ReplyCount] [int] NOT NULL,\n\t[ViewCount] [int] NOT NULL,\n\t[StartedByUserID] [int] NOT NULL,\n\t[StartedByName] [nvarchar](256) NOT NULL,\n\t[LastPostUserID] [int] NOT NULL,\n\t[LastPostName] [nvarchar](256) NOT NULL,\n\t[LastPostTime] [datetime] NOT NULL,\n\t[IsClosed] [bit] NOT NULL,\n\t[IsPinned] [bit] NOT NULL,\n\t[IsDeleted] [bit] NOT NULL,\n\t[UrlName] [nvarchar](256) NOT NULL,\n\t[AnswerPostID] [int] NULL,\n\tCONSTRAINT [PK_pf_Topic] PRIMARY KEY NONCLUSTERED \n\t\t( [TopicID] ASC ) \n);\n\nCREATE CLUSTERED INDEX [IX_pf_Topic_ForumID] ON [dbo].[pf_Topic] \n(\n\t[ForumID] ASC,\n\t[IsPinned] DESC,\n\t[LastPostTime] DESC\n);\n\nALTER TABLE [dbo].[pf_Topic]  WITH CHECK ADD  CONSTRAINT [FK_pf_Topic_pf_Forum] FOREIGN KEY([ForumID])\nREFERENCES [dbo].[pf_Forum] ([ForumID])\nON DELETE CASCADE;\n\n\nCREATE UNIQUE NONCLUSTERED INDEX [IX_pf_Topic_UrlName] ON [dbo].[pf_Topic] \n(\n\t[UrlName] ASC\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_Topic_LastPostTime] ON [dbo].[pf_Topic]\n(\n\t[LastPostTime] DESC\n);\n\n\n\n\n-- ******************************************************** pf_Post\n\nCREATE TABLE [dbo].[pf_Post](\n\t[PostID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,\n\t[TopicID] [int] NOT NULL DEFAULT (0),\n\t[ParentPostID] [int] NOT NULL DEFAULT (0),\n\t[IP] [nvarchar](40) NOT NULL,\n\t[IsFirstInTopic] [bit] NOT NULL,\n\t[ShowSig] [bit] NOT NULL,\n\t[UserID] [int] NOT NULL,\n\t[Name] [nvarchar](256) NOT NULL,\n\t[Title] [nvarchar](256) NOT NULL,\n\t[FullText] nvarchar(MAX) NOT NULL,\n\t[PostTime] [datetime] NOT NULL,\n\t[IsEdited] [bit] NOT NULL,\n\t[LastEditName] [nvarchar](256) NOT NULL,\n\t[LastEditTime] [datetime] NULL,\n\t[IsDeleted] [bit] NOT NULL,\n\t[Votes] [int] DEFAULT 0 NOT NULL\n);\n\nCREATE CLUSTERED INDEX [IX_pf_Post_TopicID] ON [dbo].[pf_Post] \n(\n\t[TopicID] ASC,\n\t[PostTime] ASC\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_Post_PostTime] ON [dbo].[pf_Post] \n(\n\t[PostTime] DESC\n);\n\nALTER TABLE [dbo].[pf_Post]  WITH CHECK ADD  CONSTRAINT [FK_pf_Post_pf_Topic] FOREIGN KEY([TopicID])\nREFERENCES [dbo].[pf_Topic] ([TopicID])\nON DELETE CASCADE;\n\nCREATE NONCLUSTERED INDEX [IX_pf_Post_UserID] ON [dbo].[pf_Post] \n(\n\t[UserID] ASC,\n\t[IsDeleted] ASC\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_Post_IP_PostTime] ON [dbo].[pf_Post] \n(\n\t[IP] ASC, [PostTime] DESC\n);\n\n\n\n\n-- ******************************************************** pf_Setting\n\nCREATE TABLE [dbo].[pf_Setting] (\n\t[Setting] [nvarchar] (256) NOT NULL PRIMARY KEY,\n\t[Value] nvarchar(MAX) NOT NULL\n);\n\n\n\n\n\n-- ******************************************************** pf_QueuedEmailMessage\n\nCREATE TABLE [dbo].[pf_QueuedEmailMessage] (\n\t[MessageID] [int] IDENTITY (1, 1) NOT NULL,\n\t[FromName] [nvarchar] (256) NOT NULL,\n\t[ToEmail] [nvarchar] (256) NOT NULL,\n\t[ToName] [nvarchar] (256) NOT NULL,\n\t[Subject] [nvarchar] (256) NOT NULL,\n\t[Body] nvarchar(MAX) NOT NULL,\n\t[HtmlBody] nvarchar(MAX) NULL,\n\t[QueueTime] [datetime] NOT NULL\n);\n\n\nALTER TABLE [dbo].[pf_QueuedEmailMessage]\n ADD CONSTRAINT [PK_pf_QueuedEmailMessage] PRIMARY KEY CLUSTERED \n(\n\t[MessageID] ASC\n);\n\n\nCREATE NONCLUSTERED INDEX [IX_pf_QueuedEmailMessage_QueueTime] ON [dbo].[pf_QueuedEmailMessage] \n(\n\t[QueueTime] ASC\n);\n\n\n\n\n\n-- ******************************************************** pf_ErrorLog\n\nCREATE TABLE [dbo].[pf_ErrorLog] (\n\t[ErrorID] [int] IDENTITY (1, 1) NOT NULL PRIMARY KEY CLUSTERED,\n\t[TimeStamp] [datetime] NULL ,\n\t[Message] nvarchar(MAX) NOT NULL ,\n\t[StackTrace] nvarchar(MAX) NOT NULL ,\n\t[Data] nvarchar(MAX) NOT NULL ,\n\t[Severity] [int] NOT NULL\n);\n\n\n\n\n\n\n\n\n\n\n\n\n\n-- ******************************************************** pf_ModerationLog\n\nCREATE TABLE [dbo].[pf_ModerationLog](\n\t[ModerationID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,\n\t[TimeStamp] [datetime] NOT NULL,\n\t[UserID] [int] NOT NULL,\n\t[UserName] [nvarchar](256) NOT NULL,\n\t[ModerationType] [int] NOT NULL,\n\t[ForumID] [int] NULL,\n\t[TopicID] [int] NOT NULL,\n\t[PostID] [int] NULL,\n\t[Comment] nvarchar(MAX) NOT NULL,\n\t[OldText] nvarchar(MAX) NULL\n);\n\n\nCREATE NONCLUSTERED INDEX [IX_pf_ModerationLog_TopicID] ON [dbo].[pf_ModerationLog] \n(\n\t[TopicID] ASC\n);\n\n\nCREATE NONCLUSTERED INDEX [IX_pf_ModerationLog_PostID] ON [dbo].[pf_ModerationLog] \n(\n\t[PostID] ASC\n);\n\n\n\n\n\n-- ******************************************************** pf_UserSession\n\nCREATE TABLE [dbo].[pf_UserSession] (\n\t[SessionID] [int] NOT NULL,\n\t[UserID] [int] NULL, \n\t[LastTime] [datetime] NOT NULL\n);\n\n\nCREATE CLUSTERED INDEX [IX_pf_UserSession_SessionID] ON [dbo].[pf_UserSession] \n(\n\t[SessionID] ASC\n);\n\n\nCREATE NONCLUSTERED INDEX [IX_pf_UserSession_UserID] ON [dbo].[pf_UserSession] \n(\n\t[UserID] ASC\n);\n\n\n\n\n\n-- ******************************************************** pf_LastForumView\n\nCREATE TABLE [dbo].[pf_LastForumView](\n\t[UserID] [int] NOT NULL,\n\t[ForumID] [int] NOT NULL,\n\t[LastForumViewDate] [datetime] NOT NULL\n);\n\n\nCREATE CLUSTERED INDEX [IX_pf_LastForumView_UserID_ForumID] ON [dbo].[pf_LastForumView] \n(\n\t[UserID] ASC,\n\t[ForumID] ASC\n);\n\n\nALTER TABLE [dbo].[pf_LastForumView]  WITH CHECK ADD  CONSTRAINT [FK_pf_LastForumView_ForumID] FOREIGN KEY([ForumID])\nREFERENCES [dbo].[pf_Forum] ([ForumID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_LastForumView]  WITH CHECK ADD  CONSTRAINT [FK_pf_LastForumView_UserID] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\n\n\n\n\n-- ******************************************************** pf_UserImages\n\nCREATE TABLE [dbo].[pf_UserImages](\n\t[UserImageID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,\n\t[UserID] [int] NOT NULL,\n\t[SortOrder] [int] NOT NULL,\n\t[IsApproved] [bit] NOT NULL,\n\t[TimeStamp] [datetime] NOT NULL,\n\t[ImageData] varbinary(MAX) NOT NULL\n);\n\n\nCREATE CLUSTERED INDEX [IX_UserImages_UserID] ON [dbo].[pf_UserImages] \n(\n\t[UserID] ASC\n);\n\n\nALTER TABLE [dbo].[pf_UserImages]  WITH CHECK ADD  CONSTRAINT [FK_pf_UserImages_pf_PopForumsUser] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\n\n\n\n\n-- ******************************************************** pf_UserAvatar\n\nCREATE TABLE [dbo].[pf_UserAvatar](\n\t[UserAvatarID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,\n\t[UserID] [int] NOT NULL,\n\t[TimeStamp] [datetime] NOT NULL,\n\t[ImageData] varbinary(MAX) NOT NULL\n);\n\n\nCREATE CLUSTERED INDEX [IX_pf_UserAvatar_UserID] ON [dbo].[pf_UserAvatar] \n(\n\t[UserID] ASC\n);\n\n\nALTER TABLE [dbo].[pf_UserAvatar]  WITH CHECK ADD  CONSTRAINT [FK_pf_UserAvatar_pf_PopForumsUser] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\n\n\n\n\n-- ******************************************************** pf_ForumPostRestrictions\n\nCREATE TABLE [dbo].[pf_ForumPostRestrictions](\n\t[ForumID] [int] NOT NULL,\n\t[Role] [nvarchar](256) NOT NULL\n);\n\n\nCREATE CLUSTERED INDEX [IX_ForumPostRestrictions_ForumID] ON [dbo].[pf_ForumPostRestrictions] \n(\n\t[ForumID] ASC\n);\n\n\nALTER TABLE [dbo].[pf_ForumPostRestrictions]  WITH CHECK ADD  CONSTRAINT [FK_pf_ForumPostRestrictions_ForumID] FOREIGN KEY([ForumID])\nREFERENCES [dbo].[pf_Forum] ([ForumID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_ForumPostRestrictions]  WITH CHECK ADD  CONSTRAINT [FK_pf_ForumPostRestrictions_Role] FOREIGN KEY([Role])\nREFERENCES [dbo].[pf_Role] ([Role])\nON DELETE CASCADE;\n\n\n\n\n\n-- ******************************************************** pf_ForumViewRestrictions\n\nCREATE TABLE [dbo].[pf_ForumViewRestrictions](\n\t[ForumID] [int] NOT NULL,\n\t[Role] [nvarchar](256) NOT NULL\n);\n\n\nCREATE CLUSTERED INDEX [IX_pf_ForumViewRestrictions_ForumID] ON [dbo].[pf_ForumViewRestrictions] \n(\n\t[ForumID] ASC\n);\n\n\nALTER TABLE [dbo].[pf_ForumViewRestrictions]  WITH CHECK ADD  CONSTRAINT [FK_pf_ForumViewRestrictions_ForumID] FOREIGN KEY([ForumID])\nREFERENCES [dbo].[pf_Forum] ([ForumID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_ForumViewRestrictions]  WITH CHECK ADD  CONSTRAINT [FK_pf_ForumViewRestrictions_Role] FOREIGN KEY([Role])\nREFERENCES [dbo].[pf_Role] ([Role])\nON DELETE CASCADE;\n\n\n\n\n\n\n-- ******************************************************** pf_TopicSearchWords\n\nCREATE TABLE [dbo].[pf_TopicSearchWords](\n\t[SearchWord] [nvarchar](256) NOT NULL,\n\t[TopicID] [int] NOT NULL,\n\t[Rank] [int] NOT NULL\n);\n\n\nCREATE CLUSTERED INDEX [IX_TopicSearchWords_SearchWord_Rank] ON [dbo].[pf_TopicSearchWords] \n(\n\t[SearchWord] ASC,\n\t[Rank] DESC\n);\n\n\nCREATE NONCLUSTERED INDEX [IX_TopicSearchWords_TopicID] ON [dbo].[pf_TopicSearchWords] \n(\n\t[TopicID] ASC\n);\n\n\n\n\n\n\n-- ******************************************************** pf_JunkWords\n\nCREATE TABLE [dbo].[pf_JunkWords](\n\t[JunkWord] [nvarchar](256) NOT NULL PRIMARY KEY\n);\n\n\n\n\n\n-- ******************************************************** pf_Favorite\n\nCREATE TABLE [dbo].[pf_Favorite](\n\t[UserID] [int] NOT NULL,\n\t[TopicID] [int] NOT NULL\n);\n\n\nALTER TABLE [dbo].[pf_Favorite]  WITH CHECK ADD  CONSTRAINT [FK_pf_Favorite_pf_PopForumsUser] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\n\nALTER TABLE [dbo].[pf_Favorite]  WITH CHECK ADD  CONSTRAINT [FK_pf_Favorite_pf_Topic] FOREIGN KEY([TopicID])\nREFERENCES [dbo].[pf_Topic] ([TopicID])\nON DELETE CASCADE;\n\n\nCREATE CLUSTERED INDEX [IX_pf_Favorite_UserID] ON [dbo].[pf_Favorite] \n(\n\t[UserID] ASC\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_Favorite_TopicID] ON [dbo].[pf_Favorite]\n(\n\t[TopicID] ASC\n);\n\n\n\n\n-- ******************************************************** pf_SubscribeTopic\n\nCREATE TABLE [dbo].[pf_SubscribeTopic](\n\t[UserID] [int] NOT NULL,\n\t[TopicID] [int] NOT NULL\n);\n\n\nALTER TABLE [dbo].[pf_SubscribeTopic]  WITH CHECK ADD  CONSTRAINT [FK_pf_SubscribeTopic_pf_PopForumsUser] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\n\nALTER TABLE [dbo].[pf_SubscribeTopic]  WITH CHECK ADD  CONSTRAINT [FK_pf_SubscribeTopic_pf_Topic] FOREIGN KEY([TopicID])\nREFERENCES [dbo].[pf_Topic] ([TopicID])\nON DELETE CASCADE;\n\n\nCREATE UNIQUE CLUSTERED INDEX [IX_pf_SubscribeTopic_UserID_TopicID] ON [dbo].[pf_SubscribeTopic] \n(\n\t[TopicID] ASC,\n\t[UserID] ASC\n);\nCREATE INDEX [IX_pf_SubscribeTopic_UserID] ON [dbo].[pf_SubscribeTopic] \n(\n\t[UserID] ASC\n);\n\n\n\n\n-- ******************************************************** pf_LastTopicView\n\nCREATE TABLE [dbo].[pf_LastTopicView](\n\t[UserID] [int] NOT NULL,\n\t[TopicID] [int] NOT NULL,\n\t[LastTopicViewDate] [datetime] NOT NULL\n);\n\n\nALTER TABLE [dbo].[pf_LastTopicView]  WITH CHECK ADD  CONSTRAINT [FK_pf_LastTopicView_TopicID] FOREIGN KEY([TopicID])\nREFERENCES [dbo].[pf_Topic] ([TopicID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_LastTopicView] CHECK CONSTRAINT [FK_pf_LastTopicView_TopicID];\n\nALTER TABLE [dbo].[pf_LastTopicView]  WITH CHECK ADD  CONSTRAINT [FK_pf_LastTopicView_UserID] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_LastTopicView] CHECK CONSTRAINT [FK_pf_LastTopicView_UserID];\n\n\nCREATE CLUSTERED INDEX [IX_LastTopicVIew_UserID] ON [dbo].[pf_LastTopicView] \n(\n\t[UserID] ASC\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_LastTopicView_TopicID] ON [dbo].[pf_LastTopicView]\n(\n\t[TopicID] ASC\n);\n\n\n\n-- **************************************************** [pf_PrivateMessage]\n\nCREATE TABLE [dbo].[pf_PrivateMessage](\n\t[PMID] [int] IDENTITY(1,1) NOT NULL,\n\t[LastPostTime] [datetime] NOT NULL,\n\t[Users] [nvarchar](MAX) NOT NULL,\n CONSTRAINT [PK_pf_PrivateMessage] PRIMARY KEY CLUSTERED \n(\n\t[PMID] ASC\n)\n);\n\n\nCREATE TABLE [dbo].[pf_PrivateMessagePost](\n\t[PMPostID] [int] IDENTITY(1,1) NOT NULL,\n\t[PMID] [int] NOT NULL,\n\t[UserID] [int] NOT NULL,\n\t[Name] [nvarchar](256) NOT NULL,\n\t[PostTime] [datetime] NOT NULL,\n\t[FullText] [nvarchar](MAX) NOT NULL\n);\nCREATE CLUSTERED INDEX [IX_pf_PrivateMessagePost_PMID] ON [dbo].[pf_PrivateMessagePost] \n(\n\t[PMID] ASC\n);\n\nALTER TABLE [dbo].[pf_PrivateMessagePost]  WITH CHECK ADD  CONSTRAINT [FK_pf_PrivateMessagePost_pf_PrivateMessage] FOREIGN KEY([PMID])\nREFERENCES [dbo].[pf_PrivateMessage] ([PMID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_PrivateMessagePost] CHECK CONSTRAINT [FK_pf_PrivateMessagePost_pf_PrivateMessage];\n\n\nCREATE TABLE [dbo].[pf_PrivateMessageUser](\n\t[PMID] [int] NOT NULL,\n\t[UserID] [int] NOT NULL,\n\t[LastViewDate] [datetime] NOT NULL,\n\t[IsArchived] [bit] NOT NULL\n);\nCREATE CLUSTERED INDEX [IX_pf_PrivateMessageUser_PMID] ON [dbo].[pf_PrivateMessageUser] \n(\n\t[PMID] ASC\n);\n\n\nALTER TABLE [dbo].[pf_PrivateMessageUser]  WITH CHECK ADD  CONSTRAINT [FK_pf_PrivateMessageUser_pf_PopForumsUser] FOREIGN KEY([UserID])\nREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_PrivateMessageUser] CHECK CONSTRAINT [FK_pf_PrivateMessageUser_pf_PopForumsUser];\n\nALTER TABLE [dbo].[pf_PrivateMessageUser]  WITH CHECK ADD  CONSTRAINT [FK_pf_PrivateMessageUser_pf_PrivateMessage] FOREIGN KEY([PMID])\nREFERENCES [dbo].[pf_PrivateMessage] ([PMID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_PrivateMessageUser] CHECK CONSTRAINT [FK_pf_PrivateMessageUser_pf_PrivateMessage];\n\n\n-- ******************************** [pf_PostVote]\n\nCREATE TABLE [dbo].[pf_PostVote](\n\t[PostID] [int] NOT NULL,\n\t[UserID] [int] NOT NULL\n);\n\nCREATE CLUSTERED INDEX [IX_pf_PostVote_PostID] ON [dbo].[pf_PostVote] \n(\n\t[PostID] ASC\n);\n\n\n\n-- ***************************** [pf_Feed]\n\nCREATE TABLE [dbo].[pf_Feed](\n\t[UserID] [int] NOT NULL,\n\t[Message] [nvarchar](max) NOT NULL,\n\t[Points] [int] NOT NULL,\n\t[TimeStamp] [datetime] NOT NULL\n);\n\nCREATE CLUSTERED INDEX [IX_pf_Feed_UserID] ON [dbo].[pf_Feed] \n(\n\t[UserID] ASC\n);\n\n\n-- ***************************** [pf_PointLedger]\nCREATE TABLE [dbo].[pf_PointLedger](\n\t[UserID] [int] NOT NULL,\n\t[EventDefinitionID] [nvarchar](256) NOT NULL,\n\t[Points] [int] NOT NULL,\n\t[TimeStamp] [datetime] NOT NULL\n);\n\nCREATE CLUSTERED INDEX [IX_pf_PointLedger_UserID] ON [dbo].[pf_PointLedger] \n(\n\t[UserID] ASC\n);\n\n\n-- ******************************** [pf_EventDefinition]\n\nCREATE TABLE [dbo].[pf_EventDefinition](\n\t[EventDefinitionID] [nvarchar](256) NOT NULL PRIMARY KEY CLUSTERED,\n\t[Description] [nvarchar](max) NOT NULL,\n\t[PointValue] [int] NOT NULL,\n\t[IsPublishedToFeed] [bit] NOT NULL\n);\n\n\n-- ****************************** [pf_AwardCalculationQueue]\n\nCREATE TABLE [dbo].[pf_AwardCalculationQueue](\n\t[Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,\n\t[Payload] [nvarchar](256) NOT NULL\n);\n\n\n-- ******************************* [pf_UserAward]\n\nCREATE TABLE [dbo].[pf_UserAward](\n\t[UserAwardID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,\n\t[UserID] [int] NOT NULL,\n\t[AwardDefinitionID] [nvarchar](256) NOT NULL,\n\t[Title] [nvarchar](256) NOT NULL,\n\t[Description] [nvarchar](max) NOT NULL,\n\t[TimeStamp] [datetime] NOT NULL\n);\n\nCREATE CLUSTERED INDEX [IX_pf_UserAward_UserID] ON [dbo].[pf_UserAward] \n(\n\t[UserID] ASC\n);\n\n\n\n-- ***************************** [pf_AwardDefinition]\n\nCREATE TABLE [dbo].[pf_AwardDefinition](\n\t[AwardDefinitionID] [nvarchar](256) NOT NULL PRIMARY KEY CLUSTERED,\n\t[Title] [nvarchar](256) NOT NULL,\n\t[Description] [nvarchar](256) NOT NULL,\n\t[IsSingleTimeAward] [bit] NOT NULL\n);\n\n\n-- ******************************* [pf_AwardCondition]\n\nCREATE TABLE [dbo].[pf_AwardCondition](\n\t[AwardDefinitionID] [nvarchar](256) NOT NULL,\n\t[EventDefinitionID] [nvarchar](256) NOT NULL,\n\t[EventCount] [int] NOT NULL\n);\n\nCREATE CLUSTERED INDEX [IX_AwardCondition_EventDefinitionID] ON [dbo].[pf_AwardCondition] \n(\n\t[EventDefinitionID] ASC\n);\n\nALTER TABLE [dbo].[pf_AwardCondition]  WITH CHECK ADD  CONSTRAINT [FK_pf_AwardCondition_pf_AwardDefinition] FOREIGN KEY([AwardDefinitionID])\nREFERENCES [dbo].[pf_AwardDefinition] ([AwardDefinitionID])\nON DELETE CASCADE;\n\nALTER TABLE [dbo].[pf_AwardCondition] CHECK CONSTRAINT [FK_pf_AwardCondition_pf_AwardDefinition];\n\n\n\n\n-- ******************************* [pf_ExternalUserAssociation]\n\nCREATE TABLE [dbo].[pf_ExternalUserAssociation](\n\t[ExternalUserAssociationID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,\n\t[UserID] [int] NOT NULL,\n\t[Issuer] [nvarchar](50) NOT NULL,\n\t[ProviderKey] [nvarchar](256) NOT NULL,\n\t[Name] [nvarchar](256) NOT NULL\n);\n\nCREATE NONCLUSTERED INDEX [IX_pf_ExternalUserAssociation_Issuer_ProviderKey] ON [dbo].[pf_ExternalUserAssociation]\n(\n\t[Issuer] ASC,\n\t[ProviderKey] ASC\n);\n\nCREATE CLUSTERED INDEX [IX_pf_ExternalUserAssociation_UserID] ON [dbo].[pf_ExternalUserAssociation]\n(\n\t[UserID] ASC\n);\n\n\n\n\nCREATE TABLE [dbo].[pf_EmailQueue](\n\t[Id] [int] IDENTITY(1,1) NOT NULL,\n\t[Payload] [nvarchar](256) NOT NULL\n);\n\nCREATE CLUSTERED INDEX IX_pf_EmailQueue_Id ON pf_EmailQueue (Id);\n\n\n\n\nCREATE TABLE [dbo].[pf_SearchQueue](\n\t[Id] [int] IDENTITY(1,1) NOT NULL,\n\t[Payload] [nvarchar](256) NOT NULL\n);\n\nCREATE CLUSTERED INDEX IX_pf_SearchQueue_ID ON pf_SearchQueue (ID);\n\n\n\nCREATE TABLE [dbo].[pf_SubNotifyQueue](\n\t[Id] [int] IDENTITY(1,1) NOT NULL,\n\t[Payload] [nvarchar](256) NOT NULL\n);\n\nCREATE CLUSTERED INDEX IX_pf_SubNotifyQueue_ID ON pf_SubNotifyQueue (ID);\n\n\n\nCREATE TABLE [dbo].[pf_ServiceHeartbeat](\n\t[ServiceName] [nvarchar](256) NOT NULL,\n\t[MachineName] [nvarchar](256) NOT NULL,\n\t[LastRun] [datetime] NOT NULL,\n);\n\n\n\nCREATE TABLE [dbo].[pf_TopicViewLog](\n\t[ID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,\n\t[UserID] [int] NULL,\n\t[TopicID] [int] NULL,\n\t[TimeStamp] [datetime] NOT NULL\n);\n\nCREATE CLUSTERED INDEX [IX_pf_TopicViewLog] ON [dbo].[pf_TopicViewLog]\n(\n\t[TimeStamp] ASC\n);\n\n\n\n\nCREATE TABLE [dbo].[pf_Notifications]\n(\n\t[UserID] INT NOT NULL, \n    [NotificationType] INT NOT NULL, \n    [ContextID] BIGINT NOT NULL, \n    [TimeStamp] DATETIME NOT NULL, \n    [IsRead] BIT NOT NULL,\n    [Data] NVARCHAR(MAX) NULL\n);\n\nCREATE CLUSTERED INDEX [IX_pf_Notifications_UserID_TimeStamp] ON [dbo].[pf_Notifications]\n(\n\tUserID, [TimeStamp] DESC\n);\nCREATE INDEX [IX_pf_Notifications_Context] ON [dbo].[pf_Notifications]\n(\n\tUserID, NotificationType, ContextID\n);\n\n\n\n\n\n\nCREATE TABLE [dbo].[pf_PostImage]\n(\n\t[ID] NVARCHAR(50) NOT NULL PRIMARY KEY, \n    [TimeStamp] DATETIME NOT NULL, \n    [TenantID] NVARCHAR(100) NULL, \n\t[ContentType] NVARCHAR(50) NOT NULL,\n    [ImageData] VARBINARY(MAX) NOT NULL\n);\n\nCREATE INDEX [IX_pf_PostImage_TenantID] ON [dbo].[pf_PostImage] ([TenantID]);\n\n\nCREATE TABLE [dbo].[pf_PostImageTemp](\n\t[PostImageTempID] [uniqueidentifier] NOT NULL PRIMARY KEY,\n\t[TimeStamp] [datetime] NOT NULL,\n    [TenantID] NVARCHAR(100) NULL\n);\nCREATE NONCLUSTERED INDEX [IX_pf_PostImageTemp_TimeStamp] ON [dbo].[pf_PostImageTemp] ([TimeStamp]);\n\n\n\n\n\nCREATE TABLE [dbo].[pf_Ignore](\n    [UserID] [int] NOT NULL,\n    [IgnoreUserID] [int] NOT NULL\n);\nALTER TABLE [dbo].[pf_Ignore] WITH CHECK ADD CONSTRAINT [FK_pf_Ignore_UserID] FOREIGN KEY([UserID])\n    REFERENCES [dbo].[pf_PopForumsUser] ([UserID])\n    ON DELETE CASCADE;\nCREATE CLUSTERED INDEX IX_pf_Ignore_UserID ON pf_Ignore (UserID, IgnoreUserID);\n\n\n\n\n\n\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('an');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('and');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('any');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('are');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('as');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('at');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('be');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('been');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('but');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('by');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('can');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('did');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('didn''t');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('do');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('does');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('don''t');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('for');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('from');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('gave');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('get');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('go');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('got');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('had');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('has');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('have');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('he');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('her');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('hers');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('here');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('his');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('i''d');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('if');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('in');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('is');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('it');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('its');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('it''s');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('i''ve');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('let''s');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('like');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('lot');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('me');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('my');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('no');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('not');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('of');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('or');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('our');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('out');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('say');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('says');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('she');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('so');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('some');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('such');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('than');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('that');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('that''s');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('the');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('their');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('there');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('they');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('the''ve');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('this');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('those');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('to');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('us');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('very');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('was');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('was''nt');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('way');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('we');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('went');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('were');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('what');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('where');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('which');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('who');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('why');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('with');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('would');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('you');\nINSERT INTO pf_JunkWords (JunkWord) VALUES ('your');\n"
  },
  {
    "path": "src/PopForums.Sql/PopForums13to14.sql",
    "content": "﻿IF OBJECT_ID('pf_EmailQueue', 'U') IS NULL\nBEGIN\nCREATE TABLE [dbo].[pf_EmailQueue](\n\t[Id] [int] IDENTITY(1,1) NOT NULL,\n\t[Payload] [nvarchar](256) NOT NULL\n) ON [PRIMARY]\n\nCREATE CLUSTERED INDEX IX_pf_EmailQueue_Id ON pf_EmailQueue (Id)\nEND\n\n\n\nIF OBJECT_ID('pf_SearchQueue', 'U') IS NULL\nBEGIN\nCREATE TABLE [dbo].[pf_SearchQueue](\n\t[ID] [int] IDENTITY(1,1) NOT NULL,\n\t[TopicID] [int] NOT NULL\n)\n\nCREATE CLUSTERED INDEX IX_pf_SearchQueue_ID ON pf_SearchQueue (ID)\nEND\n\n\n\nIF OBJECT_ID('pf_ServiceHeartbeat', 'U') IS NULL\nBEGIN\nCREATE TABLE [dbo].[pf_ServiceHeartbeat](\n\t[ServiceName] [nvarchar](256) NOT NULL,\n\t[MachineName] [nvarchar](256) NOT NULL,\n\t[LastRun] [datetime] NOT NULL,\n CONSTRAINT [PK_pf_ServiceHeartbeat] PRIMARY KEY CLUSTERED \n(\n\t[ServiceName] ASC,\n\t[MachineName] ASC\n)\n)\nEND\n\nIF EXISTS( SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'pf_Profile' AND c.name = 'AIM')\nALTER TABLE dbo.pf_Profile DROP COLUMN [AIM]\nGO\n"
  },
  {
    "path": "src/PopForums.Sql/PopForums14to15.sql",
    "content": "﻿CREATE CLUSTERED INDEX [IX_pf_Topic_ForumID] ON [dbo].[pf_Topic] \n(\n\t[ForumID] ASC,\n\t[IsPinned] DESC,\n\t[LastPostTime] DESC\n) WITH (drop_existing = on)\n\nIF IndexProperty(Object_Id('pf_Topic'), 'IX_pf_Topic_LastPostTime', 'IndexId') IS NULL\nCREATE NONCLUSTERED INDEX [IX_pf_Topic_LastPostTime] ON [dbo].[pf_Topic]\n(\n\t[LastPostTime] DESC\n)\n\nIF IndexProperty(Object_Id('pf_Favorite'), 'IX_pf_Favorite_TopicID', 'IndexId') IS NULL\nCREATE NONCLUSTERED INDEX [IX_pf_Favorite_TopicID] ON [dbo].[pf_Favorite]\n(\n\t[TopicID] ASC\n)\n\nIF IndexProperty(Object_Id('pf_LastTopicView'), 'IX_pf_LastTopicView_TopicID', 'IndexId') IS NULL\nCREATE NONCLUSTERED INDEX [IX_pf_LastTopicView_TopicID] ON [dbo].[pf_LastTopicView]\n(\n\t[TopicID] ASC\n)\n\nIF EXISTS( SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'pf_Profile' AND c.name = 'ICQ')\nALTER TABLE dbo.pf_Profile DROP COLUMN [ICQ]\n\nIF EXISTS( SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'pf_Profile' AND c.name = 'YahooMessenger')\nALTER TABLE dbo.pf_Profile DROP COLUMN [YahooMessenger]\n\nIF NOT EXISTS( SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'pf_Profile' AND c.name = 'Instagram')\nALTER TABLE dbo.pf_Profile ADD [Instagram] nvarchar(256) NULL\n\nIF IndexProperty(Object_Id('pf_SecurityLog'), 'IX_pf_SecurityLog_IP', 'IndexId') IS NOT NULL\nDROP INDEX [IX_pf_SecurityLog_IP] ON [dbo].[pf_SecurityLog]\n\nIF IndexProperty(Object_Id('pf_SecurityLog'), 'IX_pf_SecurityLog_IP_ActivityDate', 'IndexId') IS NULL\nCREATE NONCLUSTERED INDEX [IX_pf_SecurityLog_IP_ActivityDate] ON [dbo].[pf_SecurityLog] \n(\n\t[IP] ASC, [ActivityDate] DESC\n)\n\nIF IndexProperty(Object_Id('pf_SecurityLog'), 'IX_pf_SecurityLog_UserID_ActivityDate', 'IndexId') IS NULL\nCREATE NONCLUSTERED INDEX [IX_pf_SecurityLog_UserID_ActivityDate] ON [dbo].[pf_SecurityLog] \n(\n\t[UserID] ASC, [ActivityDate] DESC\n)\n\nIF IndexProperty(Object_Id('pf_SecurityLog'), 'IX_pf_SecurityLog_TargetUserID_ActivityDate', 'IndexId') IS NULL\nCREATE NONCLUSTERED INDEX [IX_pf_SecurityLog_TargetUserID_ActivityDate] ON [dbo].[pf_SecurityLog] \n(\n\t[TargetUserID] ASC, [ActivityDate] DESC\n)\n\nIF IndexProperty(Object_Id('pf_Post'), 'IX_pf_Post_IP', 'IndexId') IS NOT NULL\nDROP INDEX [IX_pf_Post_IP] ON [dbo].[pf_Post]\n\nIF IndexProperty(Object_Id('pf_Post'), 'IX_pf_Post_IP_PostTime', 'IndexId') IS NULL\nCREATE NONCLUSTERED INDEX [IX_pf_Post_IP_PostTime] ON [dbo].[pf_Post] \n(\n\t[IP] ASC, [PostTime] DESC\n) \n\n\n\nDROP TABLE [dbo].[pf_AwardCalculationQueue]\n\nCREATE TABLE [dbo].[pf_AwardCalculationQueue](\n\t[Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,\n\t[Payload] [nvarchar](256) NOT NULL\n)\n\nDROP TABLE [dbo].[pf_SearchQueue]\n\nCREATE TABLE [dbo].[pf_SearchQueue](\n\t[Id] [int] IDENTITY(1,1) NOT NULL,\n\t[Payload] [nvarchar](256) NOT NULL\n)\n\nIF IndexProperty(Object_Id('pf_Topic'), 'pf_Topic_IsIndexed_IsDeleted', 'IndexId') IS NOT NULL\nDROP INDEX [pf_Topic_IsIndexed_IsDeleted] ON [dbo].[pf_Topic]\n\nIF EXISTS( SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'pf_Topic' AND c.name = 'IsIndexed')\nALTER TABLE dbo.pf_Topic DROP COLUMN [IsIndexed]\n\nDROP TABLE [dbo].[pf_ServiceHeartbeat]\n\nCREATE TABLE [dbo].[pf_ServiceHeartbeat](\n\t[ServiceName] [nvarchar](256) NOT NULL,\n\t[MachineName] [nvarchar](256) NOT NULL,\n\t[LastRun] [datetime] NOT NULL,\n)\n\n\nIF (NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND  TABLE_NAME = 'pf_TopicViewLog'))\nBEGIN\n\tCREATE TABLE [dbo].[pf_TopicViewLog](\n\t\t[ID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY NONCLUSTERED,\n\t\t[UserID] [int] NULL,\n\t\t[TopicID] [int] NULL,\n\t\t[TimeStamp] [datetime] NOT NULL\n\t)\nEND\n\nIF IndexProperty(Object_Id('pf_TopicViewLog'), 'IX_pf_TopicViewLog_TimeStamp', 'IndexId') IS NULL\nCREATE CLUSTERED INDEX [IX_pf_TopicViewLog_TimeStamp] ON [dbo].[pf_TopicViewLog]\n(\n\t[TimeStamp] ASC\n)\n\n"
  },
  {
    "path": "src/PopForums.Sql/PopForums15to16.sql",
    "content": "﻿DROP INDEX [IX_PopForumsUser_UserName] ON [dbo].[pf_PopForumsUser]\nCREATE UNIQUE NONCLUSTERED INDEX [IX_PopForumsUser_UserName] ON [dbo].[pf_PopForumsUser]([Name])\nDROP INDEX [IX_PopForumsUser_Email] ON [dbo].[pf_PopForumsUser]\nCREATE UNIQUE NONCLUSTERED INDEX [IX_PopForumsUser_Email] ON [dbo].[pf_PopForumsUser]([Email])\n\nDELETE FROM pf_Setting WHERE [Setting] = 'TwitterConsumerKey'\nDELETE FROM pf_Setting WHERE [Setting] = 'TwitterConsumerSecret'\nDELETE FROM pf_Setting WHERE [Setting] = 'UseTwitterLogin'\n\nIF (NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND  TABLE_NAME = 'pf_UserActivity'))\nBEGIN\n\tCREATE TABLE [dbo].[pf_UserActivity](\n\t\t[UserID] [int] NOT NULL PRIMARY KEY CLUSTERED,\n\t\t[LastActivityDate] [datetime] NOT NULL,\n\t\t[LastLoginDate] [datetime] NOT NULL\n\t)\n\n\tALTER TABLE [dbo].[pf_UserActivity]  WITH CHECK ADD  CONSTRAINT [FK_pf_UserActivity_pf_PopForumsUser] FOREIGN KEY([UserID])\n\tREFERENCES [dbo].[pf_PopForumsUser] ([UserID])\n\tON DELETE CASCADE\n\n\tALTER TABLE [dbo].[pf_UserActivity] CHECK CONSTRAINT [FK_pf_UserActivity_pf_PopForumsUser]\n\n\tINSERT INTO pf_UserActivity SELECT UserID, LastActivityDate, LastLoginDate FROM pf_PopForumsUser\nEND\n\nIF EXISTS( SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'pf_PopForumsUser' AND c.name = 'LastActivityDate')\nALTER TABLE dbo.pf_PopForumsUser DROP COLUMN [LastActivityDate]\n\nIF EXISTS( SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'pf_PopForumsUser' AND c.name = 'LastLoginDate')\nALTER TABLE dbo.pf_PopForumsUser DROP COLUMN [LastLoginDate]"
  },
  {
    "path": "src/PopForums.Sql/PopForums16to21.sql",
    "content": "﻿-- For recent users view in admin\nIF INDEXPROPERTY(Object_Id('pf_SecurityLog'), 'IX_pf_SecurityLog_TargetUserID_SecurityLogType', 'IndexID') IS NULL\nBEGIN\n\tCREATE NONCLUSTERED INDEX IX_pf_SecurityLog_TargetUserID_SecurityLogType \n\tON pf_SecurityLog\n\t(\n\t\tTargetUserID DESC,\n\t\tSecurityLogType\n\t);\nEND\n\n\nDROP INDEX IF EXISTS [IX_Friend_ToUserID] ON [pf_Friend];\nDROP INDEX IF EXISTS [IX_Friend_FromUserID] ON [pf_Friend];\nIF OBJECT_ID('pf_Friend', 'U') IS NOT NULL\nBEGIN\n\tDROP TABLE pf_Friend;\nEND\n\n\nIF COL_LENGTH('dbo.pf_Profile', 'TimeZone') IS NOT NULL\nBEGIN\n\tALTER TABLE pf_Profile DROP COLUMN TimeZone;\nEND\nIF COL_LENGTH('dbo.pf_Profile', 'IsDaylightSaving') IS NOT NULL\nBEGIN\n\tALTER TABLE pf_Profile DROP COLUMN IsDaylightSaving;\nEND\n\nDELETE FROM pf_Setting WHERE Setting = 'ServerDaylightSaving' OR Setting = 'ServerTimeZone';\n\n\n\n\n\n\nIF OBJECT_ID('pf_Notifications', 'U') IS NULL\nBEGIN\n\tCREATE TABLE [dbo].[pf_Notifications]\n\t(\n\t\t[UserID] INT NOT NULL, \n\t\t[NotificationType] INT NOT NULL, \n\t\t[ContextID] BIGINT NOT NULL, \n\t\t[TimeStamp] DATETIME NOT NULL, \n\t\t[IsRead] BIT NOT NULL, \n\t\t[Data] NVARCHAR(MAX) NULL\n\t);\nEND\n\nIF INDEXPROPERTY(Object_Id('pf_Notifications'), 'IX_pf_Notifications_UserID_TimeStamp', 'IndexID') IS NULL\nBEGIN\n\tCREATE CLUSTERED INDEX [IX_pf_Notifications_UserID_TimeStamp] ON [dbo].[pf_Notifications]\n\t(\n\t\tUserID, [TimeStamp] DESC\n\t);\nEND\nIF INDEXPROPERTY(Object_Id('pf_Notifications'), 'IX_pf_Notifications_Context', 'IndexID') IS NULL\nBEGIN\n\tCREATE INDEX [IX_pf_Notifications_Context] ON [dbo].[pf_Notifications]\n\t(\n\t\tUserID, NotificationType, ContextID\n\t);\nEND\n\n\n\nIF COL_LENGTH('dbo.pf_Profile', 'IsAutoFollowOnReply') IS NULL\nBEGIN\n\tALTER TABLE pf_Profile ADD [IsAutoFollowOnReply] [bit] NOT NULL DEFAULT(1);\nEND\n\n\n\nIF COL_LENGTH('dbo.pf_SubscribeTopic', 'IsViewed') IS NOT NULL\nBEGIN\n\tALTER TABLE pf_SubscribeTopic DROP COLUMN IsViewed;\nEND\n\n\n\n\nIF INDEXPROPERTY(Object_Id('pf_SubscribeTopic'), 'IX_pf_SubscribeTopic_TopicID_UserID', 'IndexID') IS NOT NULL\nBEGIN\n\tDROP INDEX [IX_pf_SubscribeTopic_TopicID_UserID] ON [dbo].[pf_SubscribeTopic];\nEND\nIF INDEXPROPERTY(Object_Id('pf_SubscribeTopic'), 'IX_pf_SubscribeTopic_UserID_TopicID', 'IndexID') IS NULL\nBEGIN\n\tCREATE UNIQUE CLUSTERED INDEX [IX_pf_SubscribeTopic_UserID_TopicID] ON [dbo].[pf_SubscribeTopic] \n\t(\n\t\t[TopicID] ASC,\n\t\t[UserID] ASC\n\t);\nEND\nIF INDEXPROPERTY(Object_Id('pf_SubscribeTopic'), 'IX_pf_SubscribeTopic_UserID', 'IndexID') IS NULL\nBEGIN\n\tCREATE INDEX [IX_pf_SubscribeTopic_UserID] ON [dbo].[pf_SubscribeTopic] \n\t(\n\t\t[UserID] ASC\n\t);\nEND\n\n\n\nIF OBJECT_ID('pf_PostImage', 'U') IS NULL\nBEGIN\n\tCREATE TABLE [dbo].[pf_PostImage]\n\t(\n\t\t[ID] NVARCHAR(50) NOT NULL PRIMARY KEY,\n\t\t[TimeStamp] DATETIME NOT NULL,\n\t\t[TenantID] NVARCHAR(100) NULL,\n\t\t[ContentType] NVARCHAR(50) NOT NULL,\n\t\t[ImageData] VARBINARY(MAX) NOT NULL\n\t);\nEND\nIF INDEXPROPERTY(Object_Id('pf_PostImage'), 'IX_pf_PostImage_TenantID', 'IndexID') IS NULL\nBEGIN\n\tCREATE INDEX [IX_pf_PostImage_TenantID] ON [dbo].[pf_PostImage] ([TenantID]);\nEND\n\n\n\nIF OBJECT_ID('pf_PostImageTemp', 'U') IS NULL\nBEGIN\n\tCREATE TABLE [dbo].[pf_PostImageTemp](\n\t\t[PostImageTempID] [uniqueidentifier] NOT NULL PRIMARY KEY,\n\t\t[TimeStamp] [datetime] NOT NULL,\n\t\t[TenantID] NVARCHAR(100) NULL\n\t);\nEND\nIF INDEXPROPERTY(Object_Id('pf_PostImageTemp'), 'IX_pf_PostImageTemp_TimeStamp', 'IndexID') IS NULL\nBEGIN\n\tCREATE NONCLUSTERED INDEX [IX_pf_PostImageTemp_TimeStamp] ON [dbo].[pf_PostImageTemp] ([TimeStamp]);\nEND\n\n\n\n\nIF COL_LENGTH('dbo.pf_PrivateMessage', 'UserNames') IS NOT NULL\nBEGIN\n\tALTER TABLE pf_PrivateMessage DROP COLUMN UserNames;\nEND\nIF COL_LENGTH('dbo.pf_PrivateMessage', 'Subject') IS NOT NULL\nBEGIN\n\tALTER TABLE pf_PrivateMessage DROP COLUMN [Subject];\nEND\n\nIF COL_LENGTH('dbo.pf_PrivateMessage', 'Users') IS NULL\nBEGIN\n\tALTER TABLE pf_PrivateMessage ADD [Users] [nvarchar](MAX) NOT NULL DEFAULT('{}');\nEND\n\n\n\n\n\nIF OBJECT_ID('pf_SubNotifyQueue', 'U') IS NULL\nBEGIN\n\tCREATE TABLE [dbo].[pf_SubNotifyQueue](\n\t\t[Id] [int] IDENTITY(1,1) NOT NULL,\n\t\t[Payload] [nvarchar](256) NOT NULL\n\t);\nEND\nIF INDEXPROPERTY(Object_Id('pf_SubNotifyQueue'), 'IX_pf_SubNotifyQueue_ID', 'IndexID') IS NULL\nBEGIN\n\tCREATE CLUSTERED INDEX IX_pf_SubNotifyQueue_ID ON pf_SubNotifyQueue (ID);\nEND\n\n\nIF COL_LENGTH('dbo.pf_QueuedEmailMessage', 'FromEmail') IS NOT NULL\nBEGIN\n\tALTER TABLE pf_QueuedEmailMessage DROP COLUMN FromEmail;\nEND\n\n\n\nIF COL_LENGTH('dbo.pf_PopForumsUser', 'TokenExpiration') IS NULL\n    BEGIN\n        ALTER TABLE pf_PopForumsUser ADD [TokenExpiration] [datetime] NULL;\n    END\n\nIF COL_LENGTH('dbo.pf_UserActivity', 'RefreshToken') IS NULL\n    BEGIN\n        ALTER TABLE pf_UserActivity ADD [RefreshToken] [nvarchar](MAX) NULL;\n    END\n\n\n\nIF COL_LENGTH('dbo.pf_Profile', 'Twitter') IS NOT NULL\n    BEGIN\n        ALTER TABLE pf_Profile DROP COLUMN [Twitter];\n    END\n"
  },
  {
    "path": "src/PopForums.Sql/PopForums19to20.sql",
    "content": "IF COL_LENGTH('dbo.pf_PopForumsUser', 'TokenExpiration') IS NULL\n    BEGIN\n        ALTER TABLE pf_PopForumsUser ADD [TokenExpiration] [datetime] NULL;\n    END\n\nIF COL_LENGTH('dbo.pf_UserActivity', 'RefreshToken') IS NULL\n    BEGIN\n        ALTER TABLE pf_UserActivity ADD [RefreshToken] [nvarchar](MAX) NULL;\n    END"
  },
  {
    "path": "src/PopForums.Sql/PopForums20to21.sql",
    "content": "IF COL_LENGTH('dbo.pf_Profile', 'Twitter') IS NOT NULL\n    BEGIN\n        ALTER TABLE pf_Profile DROP COLUMN [Twitter];\n    END\n"
  },
  {
    "path": "src/PopForums.Sql/PopForums21to22.sql",
    "content": "IF OBJECT_ID('pf_Ignore', 'U') IS NULL\n    BEGIN\n        CREATE TABLE [dbo].[pf_Ignore](\n          [UserID] [int] NOT NULL,\n          [IgnoreUserID] [int] NOT NULL\n        );\n\n        ALTER TABLE [dbo].[pf_Ignore] WITH CHECK ADD CONSTRAINT [FK_pf_Ignore_UserID] FOREIGN KEY([UserID])\n            REFERENCES [dbo].[pf_PopForumsUser] ([UserID])\n            ON DELETE CASCADE;\n    END\nIF INDEXPROPERTY(Object_Id('pf_Ignore'), 'IX_pf_Ignore_UserID', 'IndexID') IS NULL\n    BEGIN\n        CREATE CLUSTERED INDEX IX_pf_Ignore_UserID ON pf_Ignore (UserID, IgnoreUserID);\n    END\n"
  },
  {
    "path": "src/PopForums.Sql/Repositories/AwardCalculationQueueRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class AwardCalculationQueueRepository : IAwardCalculationQueueRepository\n{\n\tpublic AwardCalculationQueueRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task Enqueue(AwardCalculationPayload payload)\n\t{\n\t\tvar serializedPayload = JsonSerializer.Serialize(payload);\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_AwardCalculationQueue (Payload) VALUES (@Payload)\", new { Payload = serializedPayload }));\n\t}\n\n\tpublic async Task<KeyValuePair<string, int>> Dequeue()\n\t{\n\t\tTask<string> serializedPayload = null;\n\t\tvar sql = @\"WITH cte AS (\nSELECT TOP(1) Payload\nFROM pf_AwardCalculationQueue WITH (ROWLOCK, READPAST)\nORDER BY Id)\nDELETE FROM cte\nOUTPUT DELETED.Payload;\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tserializedPayload = connection.QuerySingleOrDefaultAsync<string>(sql));\n\t\tif (string.IsNullOrEmpty(serializedPayload.Result))\n\t\t\treturn new KeyValuePair<string, int>();\n\t\tvar payload = JsonSerializer.Deserialize<AwardCalculationPayload>(serializedPayload.Result);\n\t\treturn new KeyValuePair<string, int>(payload.EventDefinitionID, payload.UserID);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/AwardConditionRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class AwardConditionRepository : IAwardConditionRepository\n{\n\tpublic AwardConditionRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<List<AwardCondition>> GetConditions(string awardDefinitionID)\n\t{\n\t\tTask<IEnumerable<AwardCondition>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<AwardCondition>(\"SELECT AwardDefinitionID, EventDefinitionID, EventCount FROM pf_AwardCondition WHERE AwardDefinitionID = @AwardDefinitionID\", new { AwardDefinitionID  = awardDefinitionID }));\n\t\tvar list = result.Result.ToList();\n\t\treturn list;\n\t}\n\n\tpublic async Task DeleteConditions(string awardDefinitionID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_AwardCondition WHERE AwardDefinitionID = @AwardDefinitionID\", new { AwardDefinitionID = awardDefinitionID }));\n\t}\n\n\tpublic async Task DeleteCondition(string awardDefinitionID, string eventDefinitionID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_AwardCondition WHERE AwardDefinitionID = @AwardDefinitionID AND EventDefinitionID = @EventDefinitionID\", new { AwardDefinitionID = awardDefinitionID, EventDefinitionID = eventDefinitionID }));\n\t}\n\n\tpublic async Task DeleteConditionsByEventDefinitionID(string eventDefinitionID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_AwardCondition WHERE EventDefinitionID = @EventDefinitionID\", new { EventDefinitionID = eventDefinitionID }));\n\t}\n\n\tpublic async Task SaveConditions(List<AwardCondition> conditions)\n\t{\n\t\tforeach (var condition in conditions)\n\t\t{\n\t\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_AwardCondition (AwardDefinitionID, EventDefinitionID, EventCount) VALUES (@AwardDefinitionID, @EventDefinitionID, @EventCount)\", new { condition.AwardDefinitionID, condition.EventDefinitionID, condition.EventCount }));\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/AwardDefinitionRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class AwardDefinitionRepository : IAwardDefinitionRepository\n{\n\tpublic AwardDefinitionRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<AwardDefinition> Get(string awardDefinitionID)\n\t{\n\t\tTask<AwardDefinition> awardDefinition = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tawardDefinition = connection.QuerySingleOrDefaultAsync<AwardDefinition>(\"SELECT AwardDefinitionID, Title, Description, IsSingleTimeAward FROM pf_AwardDefinition WHERE AwardDefinitionID = @AwardDefinitionID\", new { AwardDefinitionID = awardDefinitionID }));\n\t\treturn await awardDefinition;\n\t}\n\n\tpublic async Task<List<AwardDefinition>> GetAll()\n\t{\n\t\tTask<IEnumerable<AwardDefinition>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<AwardDefinition>(\"SELECT AwardDefinitionID, Title, Description, IsSingleTimeAward FROM pf_AwardDefinition ORDER BY AwardDefinitionID\"));\n\t\tvar list = result.Result.ToList();\n\t\treturn list;\n\t}\n\n\tpublic async Task<List<AwardDefinition>> GetByEventDefinitionID(string eventDefinitionID)\n\t{\n\t\tTask<IEnumerable<AwardDefinition>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<AwardDefinition>(\"SELECT D.AwardDefinitionID, D.Title, D.Description, D.IsSingleTimeAward FROM pf_AwardDefinition D JOIN pf_AwardCondition C ON D.AwardDefinitionID = C.AwardDefinitionID WHERE C.EventDefinitionID = @EventDefinitionID\", new { EventDefinitionID = eventDefinitionID }));\n\t\tvar list = result.Result.ToList();\n\t\treturn list;\n\t}\n\n\tpublic async Task Create(string awardDefinitionID, string title, string description, bool isSingleTimeAward)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_AwardDefinition (AwardDefinitionID, Title, Description, IsSingleTimeAward) VALUES (@AwardDefinitionID, @Title, @Description, @IsSingleTimeAward)\", new { AwardDefinitionID = awardDefinitionID, Title = title, Description = description.NullToEmpty(), IsSingleTimeAward = isSingleTimeAward }));\n\t}\n\n\tpublic async Task Delete(string awardDefinitionID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_AwardDefinition WHERE AwardDefinitionID = @AwardDefinitionID\", new { AwardDefinitionID = awardDefinitionID }));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/BanRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class BanRepository : IBanRepository\n{\n\tpublic BanRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task BanIP(string ip)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_IPBan (IPBan) VALUES (@IPBan)\", new { IPBan = ip }));\n\t}\n\n\tpublic async Task RemoveIPBan(string ip)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_IPBan WHERE IPBan = @IPBan\", new { IPBan = ip }));\n\t}\n\n\tpublic async Task<List<string>> GetIPBans()\n\t{\n\t\tTask<IEnumerable<string>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<string>(\"SELECT IPBan FROM pf_IPBan ORDER BY IPBan\"));\n\t\tvar list = result.Result.ToList();\n\t\treturn list;\n\t}\n\n\tpublic async Task<bool> IPIsBanned(string ip)\n\t{\n\t\tTask<IEnumerable<string>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<string>(\"SELECT * FROM pf_IPBan WHERE CHARINDEX(pf_IPBan.IPBan, @IPBan) > 0\", new { IPBan = ip }));\n\t\tvar isBanned = result.Result.Any();\n\t\treturn isBanned;\n\t}\n\n\tpublic async Task BanEmail(string email)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_EmailBan (EmailBan) VALUES (@EmailBan)\", new { EmailBan = email }));\n\t}\n\n\tpublic async Task RemoveEmailBan(string email)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_EmailBan WHERE EmailBan = @EmailBan\", new { EmailBan = email }));\n\t}\n\n\tpublic async Task<List<string>> GetEmailBans()\n\t{\n\t\tTask<IEnumerable<string>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<string>(\"SELECT EmailBan FROM pf_EmailBan ORDER BY EmailBan\"));\n\t\tvar list = result.Result.ToList();\n\t\treturn list;\n\t}\n\n\tpublic async Task<bool> EmailIsBanned(string email)\n\t{\n\t\tTask<IEnumerable<string>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<string>(\"SELECT * FROM pf_EmailBan WHERE EmailBan = @EmailBan\", new { EmailBan = email }));\n\t\tvar isBanned = result.Result.Any();\n\t\treturn isBanned;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/CategoryRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class CategoryRepository : ICategoryRepository\n{\n\tpublic CategoryRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<Category> Get(int categoryID)\n\t{\n\t\tTask<Category> category = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tcategory = connection.QuerySingleOrDefaultAsync<Category>(\"SELECT CategoryID, Title, SortOrder FROM pf_Category WHERE CategoryID = @CategoryID\", new { CategoryID = categoryID }));\n\t\treturn await category;\n\t}\n\n\tpublic async Task<List<Category>> GetAll()\n\t{\n\t\tTask<IEnumerable<Category>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tresult = connection.QueryAsync<Category>(\"SELECT CategoryID, Title, SortOrder FROM pf_Category ORDER BY SortOrder\"));\n\t\tvar list = result.Result.ToList();\n\t\treturn list;\n\t}\n\n\tpublic async Task<Category> Create(string newTitle, int sortOrder)\n\t{\n\t\tTask<int> categoryID = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tcategoryID = connection.QuerySingleAsync<int>(\"INSERT INTO pf_Category (Title, SortOrder) VALUES (@Title, @SortOrder);SELECT CAST(SCOPE_IDENTITY() as int)\", new { Title = newTitle, SortOrder = sortOrder }));\n\t\tvar category = new Category { CategoryID = categoryID.Result, Title = newTitle, SortOrder = sortOrder };\n\t\treturn category;\n\t}\n\n\tpublic async Task Delete(int categoryID)\n\t{\n\t\tTask<int> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tresult = connection.ExecuteAsync(\"DELETE FROM pf_Category WHERE CategoryID = @CategoryID\", new { CategoryID = categoryID }));\n\t\tif (result.Result != 1)\n\t\t\tthrow new Exception($\"Can't delete category with ID {categoryID} because it does not exist.\");\n\t}\n\n\tpublic async Task Update(Category category)\n\t{\n\t\tTask<int> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tresult = connection.ExecuteAsync(\"UPDATE pf_Category SET Title = @Title, SortOrder = @SortOrder WHERE CategoryID = @CategoryID\", new { category.Title, category.SortOrder, category.CategoryID }));\n\t\tif (result.Result != 1)\n\t\t\tthrow new Exception($\"Can't update category with ID {category.CategoryID} because it does not exist.\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/EmailQueueRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class EmailQueueRepository : IEmailQueueRepository\n{\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic EmailQueueRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tpublic async Task Enqueue(EmailQueuePayload payload)\n\t{\n\t\tvar serializedPayload = JsonSerializer.Serialize(payload);\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_EmailQueue (Payload) VALUES (@Payload)\", new { Payload = serializedPayload }));\n\t}\n\n\tpublic async Task<EmailQueuePayload> Dequeue()\n\t{\n\t\tTask<string> serializedPayload = null;\n\t\tvar sql = @\"WITH cte AS (\nSELECT TOP(1) Payload\nFROM pf_EmailQueue WITH (ROWLOCK, READPAST)\nORDER BY Id)\nDELETE FROM cte\nOUTPUT DELETED.Payload;\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tserializedPayload = connection.QuerySingleOrDefaultAsync<string>(sql));\n\t\tif (string.IsNullOrEmpty(serializedPayload.Result))\n\t\t\treturn null;\n\t\tvar payload = JsonSerializer.Deserialize<EmailQueuePayload>(serializedPayload.Result);\n\t\treturn payload;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/ErrorLogRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class ErrorLogRepository : IErrorLogRepository\n{\n\tpublic ErrorLogRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<ErrorLogEntry> Create(DateTime timeStamp, string message, string stackTrace, string data, ErrorSeverity severity)\n\t{\n\t\tTask<int> errorID = null;\n\t\ttry\n\t\t{\n\t\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\t\terrorID = connection.QuerySingleAsync<int>(\"INSERT INTO pf_ErrorLog (TimeStamp, Message, StackTrace, Data, Severity) VALUES (@TimeStamp, @Message, @StackTrace, @Data, @Severity);SELECT CAST(SCOPE_IDENTITY() as int)\", new {TimeStamp = timeStamp, Message = message, StackTrace = stackTrace, Data = data, Severity = severity}));\n\t\t}\n\t\tcatch (Exception exc)\n\t\t{\n\t\t\t// gross, but necessary to prevent a loop trying to record database errors to an unavailable database\n\t\t\tConsole.WriteLine($\"Can't log to database because: {exc.Message}\\r\\nOriginal error: {message}\\r\\n{stackTrace}\");\n\t\t}\n\t\tvar errorLog = new ErrorLogEntry\n\t\t{\n\t\t\tErrorID = errorID.Result,\n\t\t\tTimeStamp = timeStamp,\n\t\t\tMessage = message,\n\t\t\tStackTrace = stackTrace,\n\t\t\tData = data,\n\t\t\tSeverity = severity\n\t\t};\n\t\treturn errorLog;\n\t}\n\n\tpublic async Task<int> GetErrorCount()\n\t{\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(*) FROM pf_ErrorLog\"));\n\t\treturn await count;\n\t}\n\n\tpublic async Task<List<ErrorLogEntry>> GetErrors(int startRow, int pageSize)\n\t{\n\t\tconst string sql = @\"\nDECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH Entries AS ( \nSELECT ROW_NUMBER() OVER (ORDER BY TimeStamp DESC)\nAS Row, pf_ErrorLog.ErrorID, pf_ErrorLog.TimeStamp, pf_ErrorLog.Message, pf_ErrorLog.StackTrace, pf_ErrorLog.Data, pf_ErrorLog.Severity\nFROM pf_ErrorLog)\n\nSELECT ErrorID, TimeStamp, Message, StackTrace, Data, Severity\nFROM Entries \nWHERE Row between \n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tTask<IEnumerable<ErrorLogEntry>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<ErrorLogEntry>(sql, new { StartRow = startRow, PageSize = pageSize }));\n\t\tvar logEntries = result.Result.ToList();\n\t\treturn logEntries;\n\t}\n\n\tpublic async Task DeleteError(int errorID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_ErrorLog WHERE ErrorID = @ErrorID\", new { ErrorID = errorID }));\n\t}\n\n\tpublic async Task DeleteAllErrors()\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"TRUNCATE TABLE pf_ErrorLog\"));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/EventDefinitionRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class EventDefinitionRepository : IEventDefinitionRepository\n{\n\tpublic EventDefinitionRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<EventDefinition> Get(string eventDefinitionID)\n\t{\n\t\tTask<EventDefinition> eventDef = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\teventDef = connection.QuerySingleAsync<EventDefinition>(\"SELECT EventDefinitionID, Description, PointValue, IsPublishedToFeed FROM pf_EventDefinition WHERE EventDefinitionID = @EventDefinitionID\", new { EventDefinitionID = eventDefinitionID }));\n\t\treturn await eventDef;\n\t}\n\n\tpublic async Task<List<EventDefinition>> GetAll()\n\t{\n\t\tTask<IEnumerable<EventDefinition>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<EventDefinition>(\"SELECT EventDefinitionID, Description, PointValue, IsPublishedToFeed FROM pf_EventDefinition ORDER BY EventDefinitionID\"));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task Create(EventDefinition eventDefinition)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_EventDefinition (EventDefinitionID, Description, PointValue, IsPublishedToFeed) VALUES (@EventDefinitionID, @Description, @PointValue, @IsPublishedToFeed)\", new { eventDefinition.EventDefinitionID, Description = eventDefinition.Description.NullToEmpty(), eventDefinition.PointValue, eventDefinition.IsPublishedToFeed }));\n\t}\n\n\tpublic void Delete(string eventDefinitionID)\n\t{\n\t\t_sqlObjectFactory.GetConnection().Using(connection =>\n\t\t\tconnection.Execute(\"DELETE FROM pf_EventDefinition WHERE EventDefinitionID = @EventDefinitionID\", new { EventDefinitionID = eventDefinitionID }));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/ExternalUserAssociationRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class ExternalUserAssociationRepository : IExternalUserAssociationRepository\n{\n\tpublic ExternalUserAssociationRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<ExternalUserAssociation> Get(string issuer, string providerKey)\n\t{\n\t\tTask<ExternalUserAssociation> externalUserAssociation = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\texternalUserAssociation = connection.QuerySingleOrDefaultAsync<ExternalUserAssociation>(\"SELECT ExternalUserAssociationID, UserID, Issuer, ProviderKey, Name FROM pf_ExternalUserAssociation WHERE Issuer = @Issuer AND ProviderKey = @ProviderKey\", new {  Issuer = issuer, ProviderKey = providerKey }));\n\t\treturn await externalUserAssociation;\n\t}\n\n\tpublic async Task<ExternalUserAssociation> Get(int externalUserAssociationID)\n\t{\n\t\tTask<ExternalUserAssociation> externalUserAssociation = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\texternalUserAssociation = connection.QuerySingleOrDefaultAsync<ExternalUserAssociation>(\"SELECT ExternalUserAssociationID, UserID, Issuer, ProviderKey, Name FROM pf_ExternalUserAssociation WHERE ExternalUserAssociationID = @ExternalUserAssociationID\", new { ExternalUserAssociationID = externalUserAssociationID }));\n\t\treturn await externalUserAssociation;\n\t}\n\n\tpublic async Task<List<ExternalUserAssociation>> GetByUser(int userID)\n\t{\n\t\tTask<IEnumerable<ExternalUserAssociation>> userAssociations = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tuserAssociations = connection.QueryAsync<ExternalUserAssociation>(\"SELECT ExternalUserAssociationID, UserID, Issuer, ProviderKey, Name FROM pf_ExternalUserAssociation WHERE UserID = @UserID\", new { UserID = userID }));\n\t\treturn userAssociations.Result.ToList();\n\t}\n\n\tpublic async Task Save(int userID, string issuer, string providerKey, string name)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_ExternalUserAssociation (UserID, Issuer, ProviderKey, Name) VALUES (@UserID, @Issuer, @ProviderKey, @Name)\", new { UserID = userID, Issuer = issuer, ProviderKey = providerKey, Name = name }));\n\t}\n\n\tpublic async Task Delete(int externalUserAssociationID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_ExternalUserAssociation WHERE ExternalUserAssociationID = @ExternalUserAssociationID\", new { ExternalUserAssociationID = externalUserAssociationID }));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/FavoriteTopicsRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class FavoriteTopicsRepository : IFavoriteTopicsRepository\n{\n\tpublic FavoriteTopicsRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<List<Topic>> GetFavoriteTopics(int userID, int startRow, int pageSize)\n\t{\n\t\tTask<IEnumerable<Topic>> result = null;\n\t\tconst string sql = @\"\nDECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH Entries AS ( \nSELECT ROW_NUMBER() OVER (ORDER BY IsPinned DESC, LastPostTime DESC)\nAS Row, pf_Topic.TopicID, pf_Topic.ForumID, pf_Topic.Title, pf_Topic.ReplyCount, pf_Topic.ViewCount, \npf_Topic.StartedByUserID, pf_Topic.StartedByName, pf_Topic.LastPostUserID, pf_Topic.LastPostName, \npf_Topic.LastPostTime, pf_Topic.IsClosed, pf_Topic.IsPinned, pf_Topic.IsDeleted, pf_Topic.UrlName, pf_Topic.AnswerPostID \nFROM pf_Topic JOIN pf_Favorite F ON pf_Topic.TopicID = F.TopicID \nWHERE F.UserID = @UserID AND pf_Topic.IsDeleted = 0)\n\nSELECT TopicID, ForumID, Title, ReplyCount, ViewCount, \nStartedByUserID, StartedByName, LastPostUserID, LastPostName, \nLastPostTime, IsClosed, IsPinned, IsDeleted, UrlName, AnswerPostID\nFROM Entries \nWHERE Row between \n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<Topic>(sql, new { UserID = userID, StartRow = startRow, PageSize = pageSize }));\n\t\treturn result.Result.ToList();\n\t}\n\n\tpublic async Task<int> GetFavoriteTopicCount(int userID)\n\t{\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(*) FROM pf_Favorite F JOIN pf_Topic T ON F.TopicID = T.TopicID WHERE F.UserID = @UserID AND T.IsDeleted = 0\", new { UserID = userID }));\n\t\treturn await count;\n\t}\n\n\tpublic async Task<bool> IsTopicFavorite(int userID, int topicID)\n\t{\n\t\tTask<IEnumerable<dynamic>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync(\"SELECT * FROM pf_Favorite WHERE UserID = @UserID AND TopicID = @TopicID\", new { UserID = userID, TopicID = topicID }));\n\t\treturn result.Result.Any();\n\t}\n\n\tpublic async Task AddFavoriteTopic(int userID, int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_Favorite (UserID, TopicID) VALUES (@UserID, @TopicID)\", new { UserID = userID, TopicID = topicID }));\n\t}\n\n\tpublic async Task RemoveFavoriteTopic(int userID, int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_Favorite WHERE UserID = @UserID AND TopicID = @TopicID\", new { UserID = userID, TopicID = topicID }));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/FeedRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class FeedRepository : IFeedRepository\n{\n\tpublic FeedRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<List<FeedEvent>> GetFeed(int userID, int itemCount)\n\t{\n\t\tTask<IEnumerable<FeedEvent>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<FeedEvent>($\"SELECT TOP {itemCount} UserID, Message, Points, TimeStamp FROM pf_Feed WHERE UserID = @UserID ORDER BY TimeStamp DESC\", new { UserID = userID} ));\n\t\treturn result.Result.ToList();\n\t}\n\n\tpublic async Task<List<FeedEvent>> GetFeed(int itemCount)\n\t{\n\t\tTask<IEnumerable<FeedEvent>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<FeedEvent>($\"SELECT TOP {itemCount} UserID, Message, Points, TimeStamp FROM pf_Feed ORDER BY TimeStamp DESC\"));\n\t\treturn result.Result.ToList();\n\t}\n\n\tpublic async Task PublishEvent(int userID, string message, int points, DateTime timeStamp)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_Feed (UserID, Message, Points, TimeStamp) VALUES (@UserID, @Message, @Points, @TimeStamp)\", new { UserID = userID, Message = message, Points = points, TimeStamp = timeStamp }));\n\t}\n\n\tpublic async Task<DateTime> GetOldestTime(int userID, int takeCount)\n\t{\n\t\tvar feed = await GetFeed(userID, takeCount);\n\t\tif (feed.Count == 0)\n\t\t\treturn new DateTime(1990, 1, 1);\n\t\tvar last = feed.Last();\n\t\treturn last.TimeStamp;\n\t}\n\n\tpublic async Task DeleteOlderThan(int userID, DateTime timeCutOff)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_Feed WHERE UserID = @UserID AND TimeStamp < @TimeStamp\", new { UserID = userID, TimeStamp = timeCutOff }));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/ForumRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class ForumRepository : IForumRepository\n{\n\tpublic ForumRepository(ICacheHelper cacheHelper, ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_cacheHelper = cacheHelper;\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ICacheHelper _cacheHelper;\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\tprivate const string ForumFields = \"ForumID, CategoryID, Title, Description, IsVisible, IsArchived, SortOrder, TopicCount, PostCount, LastPostTime, LastPostName, UrlName, ForumAdapterName, IsQAForum\";\n\n\tpublic class CacheKeys\n\t{\n\t\tpublic const string ForumPostRoleRestrictions = \"PopForums.Forum.ForumPostRoleRestrictions\";\n\t\tpublic const string ForumViewRoleRestrictions = \"PopForums.Forum.ForumViewRoleRestrictions\";\n\t\tpublic const string ForumUrlNames = \"PopForums.Forum.UrlNames\";\n\t\tpublic const string ForumTitles = \"PopForums.Forum.Titles\";\n\t\tpublic const string AggregateTopicCount = \"PopForums.Forum.AggregateTopicCount\";\n\t\tpublic const string AggregatePostCount = \"PopForums.Forum.AggreatePostCount\";\n\t}\n\n\tpublic async Task<Forum> Get(int forumID)\n\t{\n\t\tTask<Forum> forum = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tforum = connection.QuerySingleOrDefaultAsync<Forum>(\"SELECT \" + ForumFields + \" FROM pf_Forum WHERE ForumID = @ForumID\", new { ForumID = forumID }));\n\t\treturn await forum;\n\t}\n\n\tpublic async Task<Forum> Get(string urlName)\n\t{\n\t\tTask<Forum> forum = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tforum = connection.QuerySingleOrDefaultAsync<Forum>(\"SELECT \" + ForumFields + \" FROM pf_Forum WHERE UrlName = @UrlName\", new { UrlName = urlName }));\n\t\treturn await forum;\n\t}\n\n\tpublic async Task<Forum> Create(int? categoryID, string title, string description, bool isVisible, bool isArchived, int sortOrder, string urlName, string forumAdapterName, bool isQAForum)\n\t{\n\t\tif (categoryID == 0)\n\t\t\tcategoryID = null;\n\t\tTask<int> forumID = null;\n\t\tvar lastPostTime = new DateTime(2000, 1, 1);\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tforumID = connection.QuerySingleAsync<int>(\"INSERT INTO pf_Forum (CategoryID, Title, Description, IsVisible, IsArchived, SortOrder, TopicCount, PostCount, LastPostTime, LastPostName, UrlName, ForumAdapterName, IsQAForum) VALUES (@CategoryID, @Title, @Description, @IsVisible, @IsArchived, @SortOrder, 0, 0, @LastPostTime, '', @UrlName, @ForumAdapterName, @IsQAForum);SELECT CAST(SCOPE_IDENTITY() as int)\", new { CategoryID = categoryID, Title = title, Description = description.NullToEmpty(), IsVisible = isVisible, IsArchived = isArchived, SortOrder = sortOrder, LastPostTime = lastPostTime, UrlName = urlName, ForumAdapterName = forumAdapterName, IsQAForum = isQAForum }));\n\t\tvar forum = new Forum\n\t\t{\n\t\t\tForumID = forumID.Result,\n\t\t\tCategoryID = categoryID,\n\t\t\tTitle = title,\n\t\t\tDescription = description,\n\t\t\tIsVisible = isVisible,\n\t\t\tIsArchived = isArchived,\n\t\t\tSortOrder = sortOrder,\n\t\t\tTopicCount = 0,\n\t\t\tPostCount = 0,\n\t\t\tLastPostTime = lastPostTime,\n\t\t\tLastPostName = String.Empty,\n\t\t\tUrlName = urlName,\n\t\t\tForumAdapterName = forumAdapterName,\n\t\t\tIsQAForum = isQAForum\n\t\t};\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumUrlNames);\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumPostRoleRestrictions);\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumViewRoleRestrictions);\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumTitles);\n\t\treturn forum;\n\t}\n\n\tpublic async Task<List<Forum>> GetForumsInCategory(int? categoryID)\n\t{\n\t\tTask<IEnumerable<Forum>> forums = null;\n\t\tif (categoryID.HasValue && categoryID != 0)\n\t\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\t\tforums = connection.QueryAsync<Forum>(\"SELECT \" + ForumFields + \" FROM pf_Forum WHERE CategoryID = @CategoryID\", new { CategoryID = categoryID.Value }));\n\t\telse\n\t\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\t\tforums = connection.QueryAsync<Forum>(\"SELECT \" + ForumFields + \" FROM pf_Forum WHERE CategoryID = 0 OR CategoryID IS NULL\"));\n\t\treturn forums.Result.ToList();\n\t}\n\n\tpublic async Task<List<string>> GetUrlNamesThatStartWith(string urlName)\n\t{\n\t\tTask<IEnumerable<string>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<string>(\"SELECT UrlName FROM pf_Forum WHERE UrlName LIKE @UrlName + '%'\", new { UrlName = urlName }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task Update(int forumID, int? categoryID, string title, string description, bool isVisible, bool isArchived, string urlName, string forumAdapterName, bool isQAForum)\n\t{\n\t\tif (categoryID == 0)\n\t\t\tcategoryID = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Forum SET CategoryID = @CategoryID, Title = @Title, Description = @Description, IsVisible = @IsVisible, IsArchived = @IsArchived, UrlName = @UrlName, ForumAdapterName = @ForumAdapterName, IsQAForum = @IsQAForum WHERE ForumID = @ForumID\", new { CategoryID = categoryID, Title = title, Description = description.NullToEmpty(), IsVisible = isVisible, IsArchived = isArchived, UrlName = urlName, ForumAdapterName = forumAdapterName, IsQAForum = isQAForum, ForumID = forumID }));\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumUrlNames);\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumTitles);\n\t}\n\n\tpublic async Task UpdateSortOrder(int forumID, int newSortOrder)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Forum SET SortOrder = @SortOrder WHERE ForumID = @ForumID\", new { SortOrder = newSortOrder, ForumID = forumID }));\n\t}\n\n\tpublic async Task UpdateCategoryAssociation(int forumID, int? categoryID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Forum SET CategoryID = @CategoryID WHERE ForumID = @ForumID\", new { CategoryID = categoryID, ForumID = forumID }));\n\t}\n\n\tpublic async Task UpdateLastTimeAndUser(int forumID, DateTime lastTime, string lastName)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Forum SET LastPostTime = @LastPostTime, LastPostName = @LastPostName WHERE ForumID = @ForumID\", new { LastPostTime = lastTime, LastPostName = lastName, ForumID = forumID }));\n\t}\n\n\tpublic async Task UpdateTopicAndPostCounts(int forumID, int topicCount, int postCount)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Forum SET TopicCount = @TopicCount, PostCount = @PostCount WHERE ForumID = @ForumID\", new { TopicCount = topicCount, PostCount = postCount, ForumID = forumID }));\n\t}\n\n\tpublic async Task IncrementPostCount(int forumID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Forum SET PostCount = PostCount + 1 WHERE ForumID = @ForumID\", new { ForumID = forumID }));\n\t}\n\n\tpublic async Task IncrementPostAndTopicCount(int forumID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Forum SET TopicCount = TopicCount + 1, PostCount = PostCount + 1 WHERE ForumID = @ForumID\", new { ForumID = forumID }));\n\t}\n\n\tpublic async Task<IEnumerable<Forum>> GetAll()\n\t{\n\t\tTask<IEnumerable<Forum>> forums = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tforums = connection.QueryAsync<Forum>(\"SELECT \" + ForumFields + \" FROM pf_Forum ORDER BY SortOrder\"));\n\t\treturn await forums;\n\t}\n\n\tpublic async Task<IEnumerable<Forum>> GetAllVisible()\n\t{\n\t\tTask<IEnumerable<Forum>> forums = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tforums = connection.QueryAsync<Forum>(\"SELECT \" + ForumFields + \" FROM pf_Forum WHERE IsVisible = 1 ORDER BY SortOrder\"));\n\t\treturn await forums;\n\t}\n\n\tpublic async Task<List<string>> GetForumPostRoles(int forumID)\n\t{\n\t\tvar restrictions = await GetForumPostRestrictionRoleGraph();\n\t\tvar roles = restrictions.Single(r => r.Key == forumID).Value;\n\t\treturn roles;\n\t}\n\n\tpublic async Task<List<string>> GetForumViewRoles(int forumID)\n\t{\n\t\tvar restrictions = await GetForumViewRestrictionRoleGraph();\n\t\tvar roles = restrictions.Single(r => r.Key == forumID).Value;\n\t\treturn roles;\n\t}\n\n\tpublic async Task<Dictionary<int, List<string>>> GetForumPostRestrictionRoleGraph()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<Dictionary<int, List<string>>>(CacheKeys.ForumPostRoleRestrictions);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject;\n\t\tvar dictionary = await GetForumRestrictionRoleGraph(\"pf_ForumPostRestrictions\");\n\t\t_cacheHelper.SetLongTermCacheObject(CacheKeys.ForumPostRoleRestrictions, dictionary);\n\t\treturn dictionary;\n\t}\n\n\tpublic async Task<Dictionary<int, List<string>>> GetForumViewRestrictionRoleGraph()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<Dictionary<int, List<string>>>(CacheKeys.ForumViewRoleRestrictions);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject;\n\t\tvar dictionary = await GetForumRestrictionRoleGraph(\"pf_ForumViewRestrictions\");\n\t\t_cacheHelper.SetLongTermCacheObject(CacheKeys.ForumViewRoleRestrictions, dictionary);\n\t\treturn dictionary;\n\t}\n\n\tprivate async Task<Dictionary<int, List<string>>> GetForumRestrictionRoleGraph(string table)\n\t{\n\t\tvar dictionary = new Dictionary<int, List<string>>();\n\t\tvar forums = await GetAll();\n\t\tforeach (var forum in forums)\n\t\t\tdictionary.Add(forum.ForumID, new List<string>());\n\t\tIEnumerable<RoleGraph> roleGraph = null;\n\t\t_sqlObjectFactory.GetConnection().Using(connection =>\n\t\t\troleGraph = connection.Query<RoleGraph>(\"SELECT ForumID, Role FROM \" + table));\n\t\tforeach (var item in roleGraph)\n\t\t{\n\t\t\tdictionary.Single(d => d.Key == item.ForumID).Value\n\t\t\t\t.Add(item.Role);\n\t\t}\n\t\treturn dictionary;\n\t}\n\n\tprivate class RoleGraph\n\t{\n\t\tpublic int ForumID { get; set; }\n\t\tpublic string Role { get; set; }\n\t}\n\n\tprivate async Task ModifyForumRole(int forumID, string role, string sql)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(sql, new { ForumID = forumID, Role = role }));\n\t}\n\n\tprivate async Task ModifyForumRole(int forumID, string sql)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(sql, new { ForumID = forumID }));\n\t}\n\n\tpublic async Task AddPostRole(int forumID, string role)\n\t{\n\t\tawait RemovePostRole(forumID, role);\n\t\tawait ModifyForumRole(forumID, role, \"INSERT INTO pf_ForumPostRestrictions (ForumID, Role) VALUES (@ForumID, @Role)\");\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumPostRoleRestrictions);\n\t}\n\n\tpublic async Task RemovePostRole(int forumID, string role)\n\t{\n\t\tawait ModifyForumRole(forumID, role, \"DELETE FROM pf_ForumPostRestrictions WHERE ForumID = @ForumID And Role = @Role\");\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumPostRoleRestrictions);\n\t}\n\n\tpublic async Task AddViewRole(int forumID, string role)\n\t{\n\t\tawait RemoveViewRole(forumID, role);\n\t\tawait ModifyForumRole(forumID, role, \"INSERT INTO pf_ForumViewRestrictions (ForumID, Role) VALUES (@ForumID, @Role)\");\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumViewRoleRestrictions);\n\t}\n\n\tpublic async Task RemoveViewRole(int forumID, string role)\n\t{\n\t\tawait ModifyForumRole(forumID, role, \"DELETE FROM pf_ForumViewRestrictions WHERE ForumID = @ForumID And Role = @Role\");\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumViewRoleRestrictions);\n\t}\n\n\tpublic async Task RemoveAllPostRoles(int forumID)\n\t{\n\t\tawait ModifyForumRole(forumID, \"DELETE FROM pf_ForumPostRestrictions WHERE ForumID = @ForumID\");\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumPostRoleRestrictions);\n\t}\n\n\tpublic async Task RemoveAllViewRoles(int forumID)\n\t{\n\t\tawait ModifyForumRole(forumID, \"DELETE FROM pf_ForumViewRestrictions WHERE ForumID = @ForumID\");\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.ForumViewRoleRestrictions);\n\t}\n\n\tpublic async Task<IEnumerable<string>> GetAllForumUrlNames()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<IEnumerable<string>>(CacheKeys.ForumUrlNames);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject;\n\t\tTask<IEnumerable<string>> urlNames = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\turlNames = connection.QueryAsync<string>(\"SELECT UrlName FROM pf_Forum\"));\n\t\t_cacheHelper.SetLongTermCacheObject(CacheKeys.ForumUrlNames, urlNames.Result);\n\t\treturn urlNames.Result;\n\t}\n\n\tpublic Dictionary<int, string> GetAllForumTitles()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<Dictionary<int, string>>(CacheKeys.ForumTitles);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject;\n\t\tDictionary<int, string> urlNames = null;\n\t\t_sqlObjectFactory.GetConnection().Using(connection =>\n\t\t\turlNames = connection.Query(\"SELECT ForumID, Title FROM pf_Forum\").ToDictionary(r => (int)r.ForumID, r => (string)r.Title));\n\t\t_cacheHelper.SetLongTermCacheObject(CacheKeys.ForumTitles, urlNames);\n\t\treturn urlNames;\n\t}\n\n\tpublic async Task<int> GetAggregateTopicCount()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<int?>(CacheKeys.AggregateTopicCount);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject.Value;\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT SUM(TopicCount) FROM pf_Forum\"));\n\t\t_cacheHelper.SetCacheObject(CacheKeys.AggregateTopicCount, count.Result);\n\t\treturn count.Result;\n\t}\n\n\tpublic async Task<int> GetAggregatePostCount()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<int?>(CacheKeys.AggregatePostCount);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject.Value;\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT SUM(PostCount) FROM pf_Forum\"));\n\t\t_cacheHelper.SetCacheObject(CacheKeys.AggregatePostCount, count.Result);\n\t\treturn count.Result;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/IgnoreRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class IgnoreRepository(ISqlObjectFactory sqlObjectFactory) : IIgnoreRepository\n{\n\tpublic async Task AddIgnore(int userID, int ignoreUserID)\n    {\n        await sqlObjectFactory.GetConnection().UsingAsync(async connection =>\n        {\n            await connection.ExecuteAsync(\n                @\"INSERT INTO pf_Ignore (UserID, IgnoreUserID) VALUES (@UserID, @IgnoreUserID)\",\n                new { UserID = userID, IgnoreUserID = ignoreUserID });\n        });\n    }\n\n    public async Task DeleteIgnore(int userID, int ignoreUserID)\n    {\n        await sqlObjectFactory.GetConnection().UsingAsync(async connection =>\n        {\n            await connection.ExecuteAsync(\n                @\"DELETE FROM pf_Ignore WHERE UserID = @UserID AND IgnoreUserID = @IgnoreUserID\",\n                new { UserID = userID, IgnoreUserID = ignoreUserID });\n        });\n    }\n\n    public async Task<IList<IgnoreWithName>> GetIgnoreList(int userID)\n    {\n\t    Task<IEnumerable<IgnoreWithName>> result = null;\n\t    await sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t    result = connection.QueryAsync<IgnoreWithName>(\"SELECT I.UserID, IgnoreUserID, Name FROM pf_Ignore I JOIN pf_PopForumsUser U ON IgnoreUserID = U.UserID WHERE I.UserID = @UserID\", new { UserID = userID }));\n\t    return result.Result.ToList();\n    }\n    \n    public async Task<List<int>> GetIgnoredUserIdsInList(int userID, List<int> userIDs)\n    {\n        if (userIDs == null || userIDs.Count == 0)\n            return new List<int>();\n        var inList = userIDs.Aggregate(string.Empty, (current, id) => current + (\",\" + id));\n        if (inList.StartsWith(\",\"))\n            inList = inList.Remove(0, 1);\n        var sql = $\"SELECT IgnoreUserID FROM pf_Ignore WHERE UserID = @UserID AND IgnoreUserID IN ({inList})\";\n        Task<IEnumerable<int>> result = null;\n        await sqlObjectFactory.GetConnection().UsingAsync(connection =>\n            result = connection.QueryAsync<int>(sql, new { UserID = userID }));\n        return result.Result.ToList();\n    }\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/LastReadRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class LastReadRepository : ILastReadRepository\n{\n\tpublic LastReadRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task SetForumRead(int userID, int forumID, DateTime readTime)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_LastForumView WHERE UserID = @UserID AND ForumID = @ForumID; INSERT INTO pf_LastForumView (UserID, ForumID, LastForumViewDate)VALUES (@UserID, @ForumID, @LastForumViewDate)\", new { UserID = userID, ForumID = forumID, LastForumViewDate = readTime }));\n\t}\n\n\tpublic async Task DeleteTopicReadsInForum(int userID, int forumID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE pf_LastTopicView FROM pf_LastTopicView JOIN pf_Topic ON pf_LastTopicView.TopicID = pf_Topic.TopicID WHERE pf_Topic.ForumID = @ForumID AND pf_LastTopicView.UserID = @UserID\", new { UserID = userID, ForumID = forumID }));\n\t}\n\n\tpublic async Task SetAllForumsRead(int userID, DateTime readTime)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_LastForumView WHERE UserID = @UserID; INSERT INTO pf_LastForumView SELECT @UserID, ForumID, @LastForumViewDate FROM pf_Forum\", new { UserID = userID, LastForumViewDate = readTime }));\n\t}\n\n\tpublic async Task DeleteAllTopicReads(int userID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_LastTopicView WHERE UserID = @UserID\", new { UserID = userID }));\n\t}\n\n\tpublic async Task SetTopicRead(int userID, int topicID, DateTime readTime)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_LastTopicView WHERE UserID = @UserID AND TopicID = @TopicID\", new { UserID = userID, TopicID = topicID }));\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_LastTopicView (UserID, TopicID, LastTopicViewDate) VALUES (@UserID, @TopicID, @LastTopicViewDate)\", new { UserID = userID, TopicID = topicID, LastTopicViewDate = readTime }));\n\t}\n\n\tpublic async Task<Dictionary<int, DateTime>> GetLastReadTimesForForums(int userID)\n\t{\n\t\tTask<IEnumerable<KeyValuePair<int, DateTime>>> dictionary = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tdictionary = connection.QueryAsync<KeyValuePair<int, DateTime>>(\"SELECT ForumID AS [Key], LastForumViewDate AS [Value] FROM pf_LastForumView WHERE UserID = @UserID\", new { UserID = userID }));\n\t\treturn dictionary.Result.ToDictionary(p => p.Key, p => p.Value);\n\t}\n\n\tpublic async Task<DateTime?> GetLastReadTimesForForum(int userID, int forumID)\n\t{\n\t\tTask<DateTime?> lastRead = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlastRead = connection.QuerySingleOrDefaultAsync<DateTime?>(\"SELECT LastForumViewDate FROM pf_LastForumView WHERE UserID = @UserID AND ForumID = @ForumID\", new { UserID = userID, ForumID = forumID }));\n\t\treturn lastRead.Result;\n\t}\n\n\tpublic async Task<Dictionary<int, DateTime>> GetLastReadTimesForTopics(int userID, IEnumerable<int> topicIDs)\n\t{\n\t\tvar dictionary = new Dictionary<int, DateTime>();\n\t\tif (!topicIDs.Any())\n\t\t\treturn dictionary;\n\t\tvar inString = new StringBuilder();\n\t\tbool isFirst = true;\n\t\tforeach (var topicID in topicIDs)\n\t\t{\n\t\t\tif (!isFirst)\n\t\t\t\tinString.Append(\", \");\n\t\t\tisFirst = false;\n\t\t\tinString.Append(topicID);\n\t\t}\n\t\tvar sql = $\"SELECT TopicID, LastTopicViewDate FROM pf_LastTopicView WHERE UserID = @UserID AND TopicID IN ({inString})\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(async connection =>\n\t\t{\n\t\t\tvar reader = await connection.ExecuteReaderAsync(sql, new {UserID = userID});\n\t\t\twhile (reader.Read())\n\t\t\t{\n\t\t\t\tvar key = reader.GetInt32(0);\n\t\t\t\tif (!dictionary.ContainsKey(key))\n\t\t\t\t\tdictionary.Add(key, reader.GetDateTime(1));\n\t\t\t}\n\t\t});\n\t\treturn dictionary;\n\t}\n\n\tpublic async Task<DateTime?> GetLastReadTimeForTopic(int userID, int topicID)\n\t{\n\t\tTask<DateTime?> time = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttime = connection.QueryFirstOrDefaultAsync<DateTime?>(\"SELECT LastTopicViewDate FROM pf_LastTopicView WHERE UserID = @UserID AND TopicID = @TopicID\", new { UserID = userID, TopicID = topicID }));\n\t\treturn time.Result;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/ModerationLogRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class ModerationLogRepository : IModerationLogRepository\n{\n\tpublic ModerationLogRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task Log(DateTime timeStamp, int userID, string userName, int moderationType, int? forumID, int topicID, int? postID, string comment, string oldText)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_ModerationLog (TimeStamp, UserID, UserName, ModerationType, ForumID, TopicID, PostID, Comment, OldText) VALUES (@TimeStamp, @UserID, @UserName, @ModerationType, @ForumID, @TopicID, @PostID, @Comment, @OldText)\", new { TimeStamp = timeStamp, UserID = userID, UserName = userName, ModerationType = moderationType, ForumID = forumID, TopicID = topicID, PostID = postID, Comment = comment.NullToEmpty(), OldText = oldText }));\n\t}\n\n\tpublic async Task<List<ModerationLogEntry>> GetLog(DateTime start, DateTime end)\n\t{\n\t\tTask<IEnumerable<ModerationLogEntry>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<ModerationLogEntry>(\"SELECT ModerationID, TimeStamp, UserID, UserName, ModerationType, ForumID, TopicID, PostID, Comment, OldText FROM pf_ModerationLog WHERE TimeStamp >= @Start AND TimeStamp <= @End ORDER BY TimeStamp\", new { Start = start, End = end }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<List<ModerationLogEntry>> GetLog(int topicID, bool excludePostEntries)\n\t{\n\t\tTask<IEnumerable<ModerationLogEntry>> list = null;\n\t\tvar sql = \"SELECT ModerationID, TimeStamp, UserID, UserName, ModerationType, ForumID, TopicID, PostID, Comment, OldText FROM pf_ModerationLog WHERE TopicID = @TopicID\";\n\t\tif (excludePostEntries)\n\t\t\tsql += \" AND PostID IS NULL\";\n\t\tsql += \"  ORDER BY TimeStamp\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<ModerationLogEntry>(sql, new { TopicID = topicID }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<List<ModerationLogEntry>> GetLog(int postID)\n\t{\n\t\tTask<IEnumerable<ModerationLogEntry>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<ModerationLogEntry>(\"SELECT ModerationID, TimeStamp, UserID, UserName, ModerationType, ForumID, TopicID, PostID, Comment, OldText FROM pf_ModerationLog WHERE PostID = @PostID ORDER BY TimeStamp\", new { PostID = postID }));\n\t\treturn list.Result.ToList();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/NotificationRepository.cs",
    "content": "﻿using PopForums.Messaging;\n\nnamespace PopForums.Sql.Repositories;\n\npublic class NotificationRepository : INotificationRepository\n{\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\tprivate readonly ICacheHelper _cacheHelper;\n\n\tpublic NotificationRepository(ISqlObjectFactory sqlObjectFactory, ICacheHelper cacheHelper)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t\t_cacheHelper = cacheHelper;\n\t\tSqlMapper.AddTypeHandler(new JsonElementTypeHandler());\n\t}\n\n\tprivate string GetCacheKey(int userID)\n\t{\n\t\treturn \"PopForums.NewNotifications.\" + userID;\n\t}\n\n\tprivate void RemoveCache(int userID)\n\t{\n\t\tvar key = GetCacheKey(userID);\n\t\t_cacheHelper.RemoveCacheObject(key);\n\t}\n\n\tpublic async Task<int> UpdateNotification(Notification notification)\n\t{\n\t\tTask<int> total = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttotal = connection.ExecuteAsync(\"UPDATE pf_Notifications SET TimeStamp = @TimeStamp, IsRead = @IsRead, Data = @Data WHERE UserID = @UserID AND NotificationType = @NotificationType AND ContextID = @ContextID\", notification));\n\t\tRemoveCache(notification.UserID);\n\t\treturn await total;\n\t}\n\n\tpublic async Task CreateNotification(Notification notification)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_Notifications (UserID, NotificationType, ContextID, TimeStamp, IsRead, Data) VALUES (@UserID, @NotificationType, @ContextID, @TimeStamp, @IsRead, @Data)\", notification));\n\t\tRemoveCache(notification.UserID);\n\t}\n\n\tpublic async Task MarkNotificationRead(int userID, NotificationType notificationType, long contextID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Notifications SET IsRead = 1 WHERE UserID = @userID AND NotificationType = @notificationType AND ContextID = @contextID\", new { userID, notificationType, contextID }));\n\t\tRemoveCache(userID);\n\t}\n\n\tpublic async Task<List<Notification>> GetNotifications(int userID, DateTime afterDateTime, int pageSize)\n\t{\n\t\tvar sql = $\"SELECT TOP {pageSize} * FROM pf_Notifications WHERE UserID = @userID AND [TimeStamp] < @afterDateTime ORDER BY [TimeStamp] DESC\";\n\t\tTask<IEnumerable<Notification>> notifications = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tnotifications = connection.QueryAsync<Notification>(sql, new { userID, afterDateTime }));\n\t\treturn notifications.Result.ToList();\n\t}\n\n\tpublic async Task<int> GetPageCount(int userID, int pageSize)\n\t{\n\t\tTask<double> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.QuerySingleAsync<double>(\"SELECT COUNT(*) FROM pf_Notifications WHERE UserID = @userID\", new { userID }));\n\t\tvar notificationCount = count.Result;\n\t\tif (notificationCount <= pageSize)\n\t\t\treturn 1;\n\t\tvar pageCount = Math.Ceiling(notificationCount / pageSize);\n\t\treturn Convert.ToInt32(pageCount);\n\t}\n\n\tpublic async Task<int> GetUnreadNotificationCount(int userID)\n\t{\n\t\tvar key = GetCacheKey(userID);\n\t\tvar cachedItem = _cacheHelper.GetCacheObject<int?>(key);\n\t\tif (cachedItem != null)\n\t\t\treturn cachedItem.Value;\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.QuerySingleAsync<int>(\"SELECT COUNT(*) FROM pf_Notifications WHERE UserID = @userID AND IsRead = 0\", new { userID }));\n\t\t_cacheHelper.SetCacheObject(key, count.Result);\n\t\treturn count.Result;\n\t}\n\n\tpublic async Task MarkAllRead(int userID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Notifications SET IsRead = 1 WHERE UserID = @userID\", new { userID }));\n\t\tRemoveCache(userID);\n\t}\n\n\tpublic async Task DeleteOlderThan(int userID, DateTime timeCutOff)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_Notifications WHERE UserID = @UserID AND TimeStamp < @TimeStamp\", new { UserID = userID, TimeStamp = timeCutOff }));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/PointLedgerRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class PointLedgerRepository : IPointLedgerRepository\n{\n\tpublic PointLedgerRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic virtual async Task RecordEntry(PointLedgerEntry entry)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_PointLedger (UserID, EventDefinitionID, Points, TimeStamp) VALUES (@UserID, @EventDefinitionID, @Points, @TimeStamp)\", new { entry.UserID, entry.EventDefinitionID, entry.Points, entry.TimeStamp }));\n\t}\n\n\tpublic async Task<int> GetPointTotal(int userID)\n\t{\n\t\tTask<int> total = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\ttotal = connection.ExecuteScalarAsync<int>(\"SELECT SUM(Points) FROM pf_PointLedger WHERE UserID = @UserID\", new { UserID = userID }));\n\t\treturn await total;\n\t}\n\n\tpublic async Task<int> GetEntryCount(int userID, string eventDefinitionID)\n\t{\n\t\tTask<int> total = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\ttotal = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(*) FROM pf_PointLedger WHERE UserID = @UserID AND EventDefinitionID = @EventDefinitionID\", new { UserID = userID, EventDefinitionID = eventDefinitionID }));\n\t\treturn await total;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/PostImageRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class PostImageRepository : IPostImageRepository\n{\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\tprivate readonly ITenantService _tenantService;\n\n\tpublic PostImageRepository(ISqlObjectFactory sqlObjectFactory, ITenantService tenantService)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t\t_tenantService = tenantService;\n\t}\n\n\tpublic async Task<PostImagePersistPayload> Persist(byte[] bytes, string contentType)\n\t{\n\t\tvar guid = Guid.NewGuid();\n\t\tvar tenantID = _tenantService.GetTenant();\n\t\tvar postImage = new PostImage\n\t\t{\n\t\t\tID = guid.ToString(),\n\t\t\tTimeStamp = DateTime.UtcNow,\n\t\t\tContentType = contentType,\n\t\t\tTenantID = tenantID,\n\t\t\tImageData = bytes\n\t\t};\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_PostImage (ID, TimeStamp, ContentType, TenantID, ImageData) VALUES (@ID, @TimeStamp, @ContentType, @TenantID, @ImageData)\", postImage));\n\t\tvar url = \"/Forums/Image/PostImage/\" + postImage.ID;\n\t\tvar payload = new PostImagePersistPayload {Url = url, ID = postImage.ID};\n\t\treturn payload;\n\t}\n\n\tpublic async Task DeletePostImageData(string id, string tenantID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_PostImage WHERE ID = @id\", new {id}));\n\t}\n\n\tpublic async Task<PostImage> GetWithoutData(string id)\n\t{\n\t\tTask<PostImage> image = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\timage = connection.QuerySingleOrDefaultAsync<PostImage>(\"SELECT ID, TimeStamp, ContentType, TenantID FROM pf_PostImage WHERE ID = @id\", new { id }));\n\t\treturn await image;\n\t}\n\n\t[Obsolete(\"Use the combination of GetWithoutData(int) and GetImageStream(int) instead.\")]\n\tpublic async Task<PostImage> Get(string id)\n\t{\n\t\tTask<PostImage> image = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\timage = connection.QuerySingleOrDefaultAsync<PostImage>(\"SELECT * FROM pf_PostImage WHERE ID = @id\", new {id}));\n\t\treturn await image;\n\t}\n\n\tpublic async Task<IStreamResponse> GetImageStream(string id)\n\t{\n\t\tvar connection = (SqlConnection)_sqlObjectFactory.GetConnection();\n\t\tvar command = new SqlCommand(\"SELECT ImageData FROM pf_PostImage WHERE ID = @id\", connection);\n\t\tcommand.Parameters.AddWithValue(\"id\", id);\n\t\tconnection.Open();\n\t\tvar reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess);\n\t\tif (await reader.ReadAsync() && !await reader.IsDBNullAsync(0))\n\t\t{\n\t\t\tvar stream = reader.GetStream(0);\n\t\t\tvar streamResponse = new StreamResponse(stream, connection, reader);\n\t\t\treturn streamResponse;\n\t\t}\n\t\treturn default;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/PostImageTempRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class PostImageTempRepository : IPostImageTempRepository\n{\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic PostImageTempRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tpublic async Task Save(Guid postImageTempID, DateTime timeStamp, string tenantID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_PostImageTemp (PostImageTempID, TimeStamp, TenantID) VALUES (@postImageTempID, @timeStamp, @tenantID)\", new { postImageTempID, timeStamp, tenantID }));\n\t}\n\n\tpublic async Task Delete(Guid id)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_PostImageTemp WHERE PostImageTempID = @id\", new { id }));\n\t}\n\n\tpublic async Task<List<Guid>> GetOld(DateTime olderThan)\n\t{\n\t\tTask<IEnumerable<Guid>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<Guid>(\"SELECT PostImageTempID FROM pf_PostImageTemp WHERE TimeStamp < @olderThan\", new { olderThan }));\n\t\treturn list.Result.ToList();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/PostRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class PostRepository : IPostRepository\n{\n\tpublic PostRepository(ISqlObjectFactory sqlObjectFactory, ICacheHelper cache)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t\t_cache = cache;\n\t}\n\n\tpublic class CacheKeys\n\t{\n\t\tpublic const string PostPages = \"PopForums.PostPages.{0}\";\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\tprivate readonly ICacheHelper _cache;\n\tprivate const string PostFields = \"PostID, TopicID, ParentPostID, IP, IsFirstInTopic, ShowSig, UserID, Name, Title, FullText, PostTime, IsEdited, LastEditName, LastEditTime, IsDeleted, Votes\";\n\n\tpublic virtual async Task<int> Create(int topicID, int parentPostID, string ip, bool isFirstInTopic, bool showSig, int userID, string name, string title, string fullText, DateTime postTime, bool isEdited, string lastEditName, DateTime? lastEditTime, bool isDeleted, int votes)\n\t{\n\t\tTask<int> postID = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tpostID = connection.QuerySingleAsync<int>(\"INSERT INTO pf_Post (TopicID, ParentPostID, IP, IsFirstInTopic, ShowSig, UserID, Name, Title, FullText, PostTime, IsEdited, LastEditName, LastEditTime, IsDeleted, Votes) VALUES (@TopicID, @ParentPostID, @IP, @IsFirstInTopic, @ShowSig, @UserID, @Name, @Title, @FullText, @PostTime, @IsEdited, @LastEditName, @LastEditTime, @IsDeleted, @Votes);SELECT CAST(SCOPE_IDENTITY() as int)\", new { TopicID = topicID, ParentPostID = parentPostID, IP = ip, IsFirstInTopic = isFirstInTopic, ShowSig = showSig, UserID = userID, Name = name, Title = title, FullText = fullText, PostTime = postTime, IsEdited = isEdited, LastEditTime = lastEditTime, LastEditName = lastEditName, IsDeleted = isDeleted, Votes = votes }));\n\t\tvar key = string.Format(CacheKeys.PostPages, topicID);\n\t\t_cache.RemoveCacheObject(key);\n\t\treturn await postID;\n\t}\n\n\tpublic async Task<bool> Update(Post post)\n\t{\n\t\tTask<int> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tresult = connection.ExecuteAsync(\"UPDATE pf_Post SET TopicID = @TopicID, ParentPostID = @ParentPostID, IP = @IP, IsFirstInTopic = @IsFirstInTopic, ShowSig = @ShowSig, UserID = @UserID, Name = @Name, Title = @Title, FullText = @FullText, PostTime = @PostTime, IsEdited = @IsEdited, LastEditName = @LastEditName, LastEditTime = @LastEditTime, IsDeleted = @IsDeleted, Votes = @Votes WHERE PostID = @PostID\", new { post.TopicID, post.ParentPostID, post.IP, post.IsFirstInTopic, post.ShowSig, post.UserID, post.Name, post.Title, post.FullText, post.PostTime, post.IsEdited, post.LastEditTime, post.LastEditName, post.IsDeleted, post.Votes, post.PostID }));\n\t\tvar key = string.Format(CacheKeys.PostPages, post.TopicID);\n\t\t_cache.RemoveCacheObject(key);\n\t\treturn result.Result == 1;\n\t}\n\n\tpublic async Task<List<Post>> Get(int topicID, bool includeDeleted, int startRow, int pageSize)\n\t{\n\t\tvar key = string.Format(CacheKeys.PostPages, topicID);\n\t\tvar page = startRow == 1 ? 1 : (startRow - 1) / pageSize + 1;\n\t\tif (!includeDeleted)\n\t\t{\n\t\t\t// we're only caching paged threads that do not include deleted posts, since only moderators\n\t\t\t// ever see threads that way, a small percentage of users\n\t\t\tvar cachedList = _cache.GetPagedListCacheObject<Post>(key, page);\n\t\t\tif (cachedList != null)\n\t\t\t\treturn cachedList;\n\t\t}\n\t\tconst string sql = @\"\nDECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH Entries AS ( \nSELECT ROW_NUMBER() OVER (ORDER BY PostTime)\nAS Row, PostID, TopicID, ParentPostID, IP, IsFirstInTopic, ShowSig, UserID, Name, Title, FullText, PostTime, IsEdited, LastEditName, LastEditTime, IsDeleted, Votes \nFROM pf_Post WHERE TopicID = @TopicID \nAND ((@IncludeDeleted = 1) OR (@IncludeDeleted = 0 AND IsDeleted = 0)))\n\nSELECT PostID, TopicID, ParentPostID, IP, IsFirstInTopic, ShowSig, UserID, Name, Title, FullText, PostTime, IsEdited, LastEditName, LastEditTime, IsDeleted, Votes\nFROM Entries \nWHERE Row between \n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tTask<IEnumerable<Post>> posts = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tposts = connection.QueryAsync<Post>(sql, new { TopicID = topicID, IncludeDeleted = includeDeleted, StartRow = startRow, PageSize = pageSize }));\n\t\tvar list = posts.Result.ToList();\n\t\tif (!includeDeleted)\n\t\t{\n\t\t\t_cache.SetPagedListCacheObject(key, page, list);\n\t\t}\n\t\treturn list;\n\t}\n\n\tpublic async Task<List<Post>> Get(int topicID, bool includeDeleted)\n\t{\n\t\tconst string sql = \"SELECT PostID, TopicID, ParentPostID, IP, IsFirstInTopic, ShowSig, UserID, Name, Title, FullText, PostTime, IsEdited, LastEditName, LastEditTime, IsDeleted, Votes FROM pf_Post WHERE TopicID = @TopicID AND ((@IncludeDeleted = 1) OR (@IncludeDeleted = 0 AND IsDeleted = 0)) ORDER BY PostTime\";\n\t\tTask<IEnumerable<Post>> posts = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tposts = connection.QueryAsync<Post>(sql, new { TopicID = topicID, IncludeDeleted = includeDeleted }));\n\t\treturn posts.Result.ToList();\n\t}\n\n\tpublic async Task<Post> GetLastInTopic(int topicID)\n\t{\n\t\tTask<Post> post = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tpost = connection.QuerySingleOrDefaultAsync<Post>(\"SELECT TOP 1 \" + PostFields + \" FROM pf_Post WHERE TopicID = @TopicID AND IsDeleted = 0 ORDER BY PostTime DESC\", new { TopicID = topicID}));\n\t\treturn await post;\n\t}\n\n\tpublic async Task<int> GetReplyCount(int topicID, bool includeDeleted)\n\t{\n\t\tvar sql = \"SELECT COUNT(*) FROM pf_Post WHERE TopicID = @TopicID\";\n\t\tif (!includeDeleted)\n\t\t\tsql += \" AND IsDeleted = 0 AND IsFirstInTopic = 0\";\n\t\tTask<int> replyCount = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\treplyCount = connection.ExecuteScalarAsync<int>(sql, new { TopicID = topicID }));\n\t\treturn await replyCount;\n\t}\n\n\tpublic async Task<Post> Get(int postID)\n\t{\n\t\tTask<Post> post = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tpost = connection.QuerySingleOrDefaultAsync<Post>(\"SELECT \" + PostFields + \" FROM pf_Post WHERE PostID = @PostID\", new { PostID = postID }));\n\t\treturn await post;\n\t}\n\n\tpublic async Task<Dictionary<int, DateTime>> GetPostIDsWithTimes(int topicID, bool includeDeleted)\n\t{\n\t\tTask<IEnumerable<dynamic>> results = null;\n\t\tvar sql = \"SELECT PostID, PostTime FROM pf_Post WHERE TopicID = @TopicID\";\n\t\tif (!includeDeleted)\n\t\t\tsql += \" AND IsDeleted = 0\";\n\t\tsql += \" ORDER BY PostTime\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresults = connection.QueryAsync(sql, new { TopicID = topicID }));\n\t\tvar dictionary = results.Result.ToDictionary(r => (int) r.PostID, r => (DateTime) r.PostTime);\n\t\treturn dictionary;\n\t}\n\n\tpublic async Task<int> GetPostCount(int userID)\n\t{\n\t\tTask<int> postCount = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tpostCount = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(PostID) FROM pf_Post JOIN pf_Topic ON pf_Post.TopicID = pf_Topic.TopicID WHERE pf_Post.UserID = @UserID AND pf_Post.IsDeleted = 0 AND pf_Topic.IsDeleted = 0\", new { UserID = userID }));\n\t\treturn await postCount;\n\t}\n\n\tpublic async Task<List<IPHistoryEvent>> GetIPHistory(string ip, DateTime start, DateTime end)\n\t{\n\t\tTask<IEnumerable<IPHistoryEvent>> events = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tevents = connection.QueryAsync<IPHistoryEvent>(\"SELECT PostID AS ID, PostTime AS EventTime, UserID, Name, Title AS Description FROM pf_Post WHERE IP = @IP AND PostTime >= @Start AND PostTime <= @End\", new { IP = ip, Start = start, End = end }));\n\t\tvar list = events.Result.ToList();\n\t\tforeach (var item in list)\n\t\t\titem.Type = \"Post\";\n\t\treturn list;\n\t}\n\n\tpublic async Task<int> GetLastPostID(int topicID)\n\t{\n\t\tconst string sql = \"SELECT PostID FROM pf_Post WHERE TopicID = @TopicID AND IsDeleted = 0 ORDER BY PostTime DESC\";\n\t\tTask<int> id = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tid = connection.QuerySingleOrDefaultAsync<int>(sql, new { TopicID = topicID }));\n\t\treturn await id;\n\t}\n\n\tpublic async Task<int> GetVoteCount(int postID)\n\t{\n\t\tconst string sql = \"SELECT Votes FROM pf_Post WHERE PostID = @PostID\";\n\t\tTask<int> votes = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tvotes = connection.QuerySingleOrDefaultAsync<int>(sql, new { PostID = postID }));\n\t\treturn await votes;\n\t}\n\n\tpublic async Task<int> CalculateVoteCount(int postID)\n\t{\n\t\tconst string sql = \"SELECT COUNT(*) FROM pf_PostVote WHERE PostID = @PostID\";\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tcount = connection.ExecuteScalarAsync<int>(sql, new { PostID = postID }));\n\t\treturn await count;\n\t}\n\n\tpublic async Task SetVoteCount(int postID, int votes)\n\t{\n\t\tconst string sql = \"UPDATE pf_Post SET Votes = @Votes WHERE PostID = @PostID\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(sql, new { Votes = votes, PostID = postID }));\n\t}\n\n\tpublic async Task VotePost(int postID, int userID)\n\t{\n\t\tconst string sql = \"INSERT INTO pf_PostVote (PostID, UserID) VALUES (@PostID, @UserID)\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(sql, new { PostID = postID, UserID = userID }));\n\t}\n\n\tpublic async Task<Dictionary<int, string>> GetVotes(int postID)\n\t{\n\t\tTask<IEnumerable<dynamic>> results = null;\n\t\tconst string sql = \"SELECT V.UserID, U.Name FROM pf_PostVote V LEFT JOIN pf_PopForumsUser U ON V.UserID = U.UserID WHERE V.PostID = @PostID\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresults = connection.QueryAsync(sql, new { PostID = postID }));\n\t\tvar dictionary = results.Result.ToDictionary(r => (int) r.UserID, r => (string) r.Name);\n\t\treturn dictionary;\n\t}\n\n\tpublic async Task<List<int>> GetVotedPostIDs(int userID, List<int> postIDs)\n\t{\n\t\tTask<IEnumerable<int>> result = null;\n\t\tif (postIDs.Count == 0)\n\t\t\treturn new List<int>();\n\t\tvar inList = postIDs.Aggregate(string.Empty, (current, postID) => current + (\",\" + postID));\n\t\tif (inList.StartsWith(\",\"))\n\t\t\tinList = inList.Remove(0, 1);\n\t\tvar sql = $\"SELECT PostID FROM pf_PostVote WHERE PostID IN ({inList}) AND UserID = @UserID\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<int>(sql, new { UserID = userID }));\n\t\tvar list = result.Result.ToList();\n\t\treturn list;\n\t}\n\n\tpublic async Task DeleteVote(int postID, int userID)\n\t{\n\t\tconst string sql = \"DELETE FROM pf_PostVote WHERE PostID = @postID AND UserID = @userID\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(sql, new { postID, userID }));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/PrivateMessageRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class PrivateMessageRepository : IPrivateMessageRepository\n{\n\tpublic PrivateMessageRepository(ICacheHelper cacheHelper, ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_cacheHelper = cacheHelper;\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t\tSqlMapper.AddTypeHandler(new JsonElementTypeHandler());\n\t}\n\n\tprivate readonly ICacheHelper _cacheHelper;\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic class CacheKeys\n\t{\n\t\tpublic static string PMCount(int userID)\n\t\t{\n\t\t\treturn \"PopForums.PrivateMessages.Count.\" + userID;\n\t\t}\n\t}\n\n\tpublic async Task<PrivateMessage> Get(int pmID, int userID)\n\t{\n\t\tTask<PrivateMessage> pm = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tpm = connection.QuerySingleOrDefaultAsync<PrivateMessage>(\"SELECT P.PMID, LastPostTime, Users, U.LastViewDate FROM pf_PrivateMessage P JOIN pf_PrivateMessageUser U ON P.PMID = U.PMID WHERE P.PMID = @pmID AND U.UserID = @userID\", new { pmID, userID }));\n\t\treturn await pm;\n\t}\n\n\tpublic async Task<int> GetExistingFromIDs(List<int> ids)\n\t{\n\t\tTask<int> result = null;\n\t\tvar count = ids.Count;\n\t\tvar array = string.Join(\", \", ids);\n\t\tvar sql = @$\"SELECT PMID\nFROM pf_PrivateMessageUser \nGROUP BY PMID \nHAVING SUM(CASE WHEN UserID IN ({array}) THEN 1 ELSE 0 END) = {count}\nAND COUNT(*) = {count}\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryFirstOrDefaultAsync<int>(sql));\n\t\treturn await result;\n\t}\n\n\tpublic async Task<List<PrivateMessagePost>> GetPosts(int pmID, DateTime afterDateTime)\n\t{\n\t\tvar sql = $\"SELECT * FROM pf_PrivateMessagePost WHERE PMID = @pmID AND PostTime > @afterDateTime ORDER BY PostTime\";\n\t\tTask<IEnumerable<PrivateMessagePost>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<PrivateMessagePost>(sql, new { pmID, afterDateTime }));\n\t\treturn result.Result.ToList();\n\t}\n\n\tpublic async Task<List<PrivateMessagePost>> GetPosts(int pmID, DateTime beforeDateTime, int pageSize)\n\t{\n\t\tvar sql = $\"SELECT TOP {pageSize} * FROM pf_PrivateMessagePost WHERE PMID = @pmID AND PostTime < @beforeDateTime ORDER BY PostTime DESC\";\n\t\tTask<IEnumerable<PrivateMessagePost>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<PrivateMessagePost>(sql, new { pmID, beforeDateTime }));\n\t\tvar list = result.Result.Reverse().ToList();\n\t\treturn list;\n\t}\n\n\tpublic virtual async Task<int> CreatePrivateMessage(PrivateMessage pm)\n\t{\n\t\tTask<int> id = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tid = connection.QuerySingleAsync<int>(\"INSERT INTO pf_PrivateMessage (LastPostTime, Users) VALUES (@LastPostTime, @Users);SELECT CAST(SCOPE_IDENTITY() as int)\", new { pm.LastPostTime, pm.Users }));\n\t\tpm.PMID = await id;\n\t\treturn pm.PMID;\n\t}\n\n\tpublic async Task AddUsers(int pmID, List<int> userIDs, DateTime viewDate, bool isArchived)\n\t{\n\t\tforeach (var id in userIDs)\n\t\t{\n\t\t\t_cacheHelper.RemoveCacheObject(CacheKeys.PMCount(id));\n\t\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_PrivateMessageUser (PMID, UserID, LastViewDate, IsArchived) VALUES (@PMID, @UserID, @LastViewDate, @IsArchived)\", new { PMID = pmID, UserID = id, LastViewDate = viewDate, IsArchived = isArchived }));\n\t\t}\n\t}\n\n\tpublic virtual async Task<int> AddPost(PrivateMessagePost post)\n\t{\n\t\tvar users = await GetUsers(post.PMID);\n\t\tforeach (var user in users)\n\t\t\t_cacheHelper.RemoveCacheObject(CacheKeys.PMCount(user.UserID));\n\t\tTask<int> id = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tid = connection.QuerySingleAsync<int>(\"INSERT INTO pf_PrivateMessagePost (PMID, UserID, Name, PostTime, FullText) VALUES (@PMID, @UserID, @Name, @PostTime, @FullText);SELECT CAST(SCOPE_IDENTITY() as int)\", new { post.PMID, post.UserID, post.Name, post.PostTime, post.FullText }));\n\t\tpost.PMPostID = await id;\n\t\treturn post.PMPostID;\n\t}\n\n\tpublic async Task<List<PrivateMessageUser>> GetUsers(int pmID)\n\t{\n\t\tTask<IEnumerable<PrivateMessageUser>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<PrivateMessageUser>(\"SELECT PMID, UserID, LastViewDate, IsArchived FROM pf_PrivateMessageUser WHERE PMID = @PMID\", new { PMID = pmID }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task SetLastViewTime(int pmID, int userID, DateTime viewDate)\n\t{\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.PMCount(userID));\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PrivateMessageUser SET LastViewDate = @LastViewDate WHERE UserID = @UserID AND PMID = @PMID\", new { LastViewDate = viewDate, UserID = userID, PMID = pmID }));\n\t}\n\n\tpublic async Task SetArchive(int pmID, int userID, bool isArchived)\n\t{\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.PMCount(userID));\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PrivateMessageUser SET IsArchived = @IsArchived WHERE UserID = @UserID AND PMID = @PMID\", new { IsArchived = isArchived, UserID = userID, PMID = pmID }));\n\t}\n\n\tpublic async Task<List<PrivateMessage>> GetPrivateMessages(int userID, PrivateMessageBoxType boxType, int startRow, int pageSize)\n\t{\n\t\tvar isArchived = boxType == PrivateMessageBoxType.Archive;\n\t\tconst string sql =\n\t\t\t@\"DECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH Entries AS ( \n\tSELECT ROW_NUMBER() OVER (ORDER BY [LastPostTime] DESC)\n\tAS Row, P.PMID, LastPostTime, Users, U.LastViewDate \n\tFROM pf_PrivateMessage P JOIN pf_PrivateMessageUser U \n\tON P.PMID = U.PMID WHERE U.UserID = @UserID \n\tAND U.IsArchived = @IsArchived)\n\nSELECT PMID, LastPostTime, Users, LastViewDate\nFROM Entries \nWHERE Row between \n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tTask<IEnumerable<PrivateMessage>> messsages = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tmesssages = connection.QueryAsync<PrivateMessage>(sql, new { StartRow = startRow, PageSize = pageSize, UserID = userID, IsArchived = isArchived }));\n\t\treturn messsages.Result.ToList();\n\t}\n\n\tpublic async Task<int> GetBoxCount(int userID, PrivateMessageBoxType boxType)\n\t{\n\t\tvar isArchived = boxType == PrivateMessageBoxType.Archive;\n\t\tvar sql = \"SELECT COUNT(*) FROM pf_PrivateMessage P JOIN pf_PrivateMessageUser U ON P.PMID = U.PMID WHERE U.UserID = @UserID AND U.IsArchived = @IsArchived\";\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(sql, new { UserID = userID, IsArchived = isArchived }));\n\t\treturn await count;\n\t}\n\n\tpublic async Task<int> GetUnreadCount(int userID)\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<int?>(CacheKeys.PMCount(userID));\n\t\tif (cacheObject.HasValue)\n\t\t\treturn cacheObject.Value;\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(P.PMID) FROM pf_PrivateMessage P JOIN pf_PrivateMessageUser U ON P.PMID = U.PMID WHERE LastPostTime > LastViewDate AND U.UserID = @UserID AND U.IsArchived = 0\", new { UserID = userID }));\n\t\t_cacheHelper.SetCacheObject(CacheKeys.PMCount(userID), count.Result);\n\t\treturn await count;\n\t}\n\n\tpublic async Task UpdateLastPostTime(int pmID, DateTime lastPostTime)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PrivateMessage SET LastPostTime = @LastPostTime WHERE PMID = @PMID\", new { LastPostTime = lastPostTime, PMID = pmID }));\n\t}\n\n\tpublic async Task<int?> GetFirstUnreadPostID(int pmID, DateTime lastReadTime)\n\t{\n\t\tTask<int?> pmPostID = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tpmPostID = connection.QueryFirstOrDefaultAsync<int?>(\"SELECT TOP 1 PMPostID FROM pf_PrivateMessagePost WHERE PostTime > @lastReadTime AND PMID = @pmID ORDER BY PostTime\", new { lastReadTime, pmID }));\n\t\treturn await pmPostID;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/ProfileRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class ProfileRepository : IProfileRepository\n{\n\tpublic ProfileRepository(ICacheHelper cacheHelper, ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_cacheHelper = cacheHelper;\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ICacheHelper _cacheHelper;\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic class CacheKeys\n\t{\n\t\tpublic static string UserProfile(int userID)\n\t\t{\n\t\t\treturn \"PopForums.Profile.User.\" + userID;\n\t\t}\n\t}\n\n\tpublic async Task<Profile> GetProfile(int userID)\n\t{\n\t\tvar key = CacheKeys.UserProfile(userID);\n\t\tvar cachedItem = _cacheHelper.GetCacheObject<Profile>(key);\n\t\tif (cachedItem != null)\n\t\t\treturn cachedItem;\n\t\tTask<Profile> profile = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tprofile = connection.QuerySingleOrDefaultAsync<Profile>(\"SELECT UserID, IsSubscribed, Signature, ShowDetails, Location, IsPlainText, DOB, Web, Facebook, Instagram, IsTos, AvatarID, ImageID, HideVanity, LastPostID, Points, IsAutoFollowOnReply FROM pf_Profile WHERE UserID = @UserID\", new { UserID = userID }));\n\t\tif (profile.Result != null)\n\t\t\t_cacheHelper.SetCacheObject(key, profile.Result);\n\t\treturn await profile;\n\t}\n\n\tpublic async Task Create(Profile profile)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_Profile (UserID, IsSubscribed, Signature, ShowDetails, Location, IsPlainText, DOB, Web, Facebook, Instagram, IsTos, AvatarID, ImageID, HideVanity, LastPostID, Points, IsAutoFollowOnReply) VALUES (@UserID, @IsSubscribed, @Signature, @ShowDetails, @Location, @IsPlainText, @DOB, @Web, @Facebook, @Instagram, @IsTos, @AvatarID, @ImageID, @HideVanity, @LastPostID, @Points, @IsAutoFollowOnReply)\", new { profile.UserID, profile.IsSubscribed, Signature = profile.Signature.NullToEmpty(), profile.ShowDetails, Location = profile.Location.NullToEmpty(), profile.IsPlainText, profile.Dob, Web = profile.Web.NullToEmpty(), Instagram =  profile.Instagram.NullToEmpty(), Facebook = profile.Facebook.NullToEmpty(), profile.IsTos, profile.AvatarID, profile.ImageID, profile.HideVanity, profile.LastPostID, profile.Points, profile.IsAutoFollowOnReply }));\n\t}\n\n\tpublic async Task<bool> Update(Profile profile)\n\t{\n\t\tTask<int> success = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tsuccess = connection.ExecuteAsync(\"UPDATE pf_Profile SET IsSubscribed = @IsSubscribed, Signature = @Signature, ShowDetails = @ShowDetails, Location = @Location, IsPlainText = @IsPlainText, DOB = @DOB, Web = @Web, Facebook = @Facebook, Instagram = @Instagram, IsTos = @IsTos, AvatarID = @AvatarID, ImageID = @ImageID, HideVanity = @HideVanity, LastPostID = @LastPostID, Points = @Points, IsAutoFollowOnReply = @IsAutoFollowOnReply WHERE UserID = @UserID\", new { profile.UserID, profile.IsSubscribed, Signature = profile.Signature.NullToEmpty(), profile.ShowDetails, Location = profile.Location.NullToEmpty(), profile.IsPlainText, profile.Dob, Web = profile.Web.NullToEmpty(), Instagram = profile.Instagram.NullToEmpty(), Facebook = profile.Facebook.NullToEmpty(), IsTos = profile.IsTos, profile.AvatarID, profile.ImageID, profile.HideVanity, profile.LastPostID, profile.Points, profile.IsAutoFollowOnReply }));\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.UserProfile(profile.UserID));\n\t\treturn success.Result == 1;\n\t}\n\n\tpublic async Task<int?> GetLastPostID(int userID)\n\t{\n\t\tTask<int?> postID = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tpostID = connection.QuerySingleOrDefaultAsync<int?>(\"SELECT LastPostID FROM pf_Profile WHERE UserID = @UserID\", new { UserID = userID }));\n\t\treturn await postID;\n\t}\n\n\tpublic async Task<bool> SetLastPostID(int userID, int postID)\n\t{\n\t\tTask<int> success = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tsuccess = connection.ExecuteAsync(\"UPDATE pf_Profile SET LastPostID = @LastPostID WHERE UserID = @UserID\", new { LastPostID = postID, UserID = userID }));\n\t\treturn success.Result == 1;\n\t}\n\n\tpublic async Task<Dictionary<int, string>> GetSignatures(List<int> userIDs)\n\t{\n\t\tif (userIDs.Count == 0)\n\t\t\treturn new Dictionary<int, string>();\n\t\tvar inList = userIDs.Aggregate(string.Empty, (current, userID) => current + (\",\" + userID));\n\t\tif (inList.StartsWith(\",\"))\n\t\t\tinList = inList.Remove(0, 1);\n\t\tvar sql = $\"SELECT UserID, Signature FROM pf_Profile WHERE NOT Signature = '' AND UserID IN ({inList})\";\n\t\tTask<IEnumerable<dynamic>> dictionary = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tdictionary = connection.QueryAsync(sql));\n\t\treturn dictionary.Result.ToDictionary(r => (int)r.UserID, r => (string)r.Signature);\n\t}\n\n\tpublic async Task<Dictionary<int, int>> GetAvatars(List<int> userIDs)\n\t{\n\t\tif (userIDs.Count == 0)\n\t\t\treturn new Dictionary<int, int>();\n\t\tvar inList = userIDs.Aggregate(string.Empty, (current, userID) => current + (\",\" + userID));\n\t\tif (inList.StartsWith(\",\"))\n\t\t\tinList = inList.Remove(0, 1);\n\t\tvar sql = $\"SELECT UserID, AvatarID FROM pf_Profile WHERE NOT AvatarID IS NULL AND UserID IN ({inList})\";\n\t\tTask<IEnumerable<dynamic>> dictionary = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tdictionary = connection.QueryAsync(sql));\n\t\treturn dictionary.Result.ToDictionary(r => (int)r.UserID, r => (int)r.AvatarID);\n\t}\n\n\tpublic async Task SetCurrentImageIDToNull(int userID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Profile SET ImageID = NULL WHERE UserID = @UserID\", new { UserID = userID }));\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.UserProfile(userID));\n\t}\n\n\tpublic async Task UpdatePoints(int userID, int points)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Profile SET Points = @Points WHERE UserID = @UserID\", new { UserID = userID, Points = points }));\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.UserProfile(userID));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/QueuedEmailMessageRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class QueuedEmailMessageRepository : IQueuedEmailMessageRepository\n{\n\tpublic QueuedEmailMessageRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<int> CreateMessage(QueuedEmailMessage message)\n\t{\n\t\tTask<int> id = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tid = connection.QuerySingleAsync<int>(\"INSERT INTO pf_QueuedEmailMessage (FromName, ToEmail, ToName, Subject, Body, HtmlBody, QueueTime) VALUES (@FromName, @ToEmail, @ToName, @Subject, @Body, @HtmlBody, @QueueTime);SELECT CAST(SCOPE_IDENTITY() as int)\", new { message.FromName, message.ToEmail, message.ToName, message.Subject, message.Body, message.HtmlBody, message.QueueTime }));\n\t\tif (id.Result == 0)\n\t\t\tthrow new Exception(\"MessageID was not returned from creation of a QueuedEmailMessage.\");\n\t\treturn await id;\n\t}\n\n\tpublic async Task DeleteMessage(int messageID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_QueuedEmailMessage WHERE MessageID = @MessageID\", new { MessageID = messageID }));\n\t}\n\n\tpublic async Task<QueuedEmailMessage> GetMessage(int messageID)\n\t{\n\t\tTask<QueuedEmailMessage> message = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tmessage = connection.QuerySingleOrDefaultAsync<QueuedEmailMessage>(\"SELECT MessageID, FromName, ToEmail, ToName, Subject, Body, HtmlBody, QueueTime FROM pf_QueuedEmailMessage WHERE MessageID = @MessageID\", new {messageID}));\n\t\treturn await message;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/RoleRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class RoleRepository : IRoleRepository\n{\n\tpublic RoleRepository(ICacheHelper cacheHelper, ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_cacheHelper = cacheHelper;\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ICacheHelper _cacheHelper;\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic class CacheKeys\n\t{\n\t\tpublic const string AllRoles = \"PopForums.Roles.All\";\n\t\tpublic static string UserRole(int userID)\n\t\t{\n\t\t\treturn \"PopForums.Roles.User.\" + userID;\n\t\t}\n\t}\n\n\tpublic async Task CreateRole(string role)\n\t{\n\t\tTask<IEnumerable<string>> exists = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\texists = connection.QueryAsync<string>(\"SELECT Role FROM pf_Role WHERE Role LIKE @Role\", new { Role = role }));\n\t\tif (exists.Result.Any())\n\t\t\treturn;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_Role (Role) VALUES (@Role)\", new { Role = role }));\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.AllRoles);\n\t}\n\n\tpublic async Task<bool> DeleteRole(string role)\n\t{\n\t\tTask<int> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tresult = connection.ExecuteAsync(\"DELETE FROM pf_Role WHERE Role = @Role\", new { Role = role }));\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.AllRoles);\n\t\treturn result.Result == 1;\n\t}\n\n\tpublic async Task<List<string>> GetAllRoles()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<List<string>>(CacheKeys.AllRoles);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject;\n\t\tTask<IEnumerable<string>> roles = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\troles = connection.QueryAsync<string>(\"SELECT Role FROM pf_Role ORDER BY Role\"));\n\t\t_cacheHelper.SetCacheObject(CacheKeys.AllRoles, roles.Result);\n\t\treturn roles.Result.ToList();\n\t}\n\n\tpublic async Task<List<string>> GetUserRoles(int userID)\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<List<string>>(CacheKeys.UserRole(userID));\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject;\n\t\tTask<IEnumerable<string>> roles = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\troles = connection.QueryAsync<string>(\"SELECT Role FROM pf_PopForumsUserRole WHERE UserID = @UserID\", new { UserID = userID }));\n\t\tvar list = roles.Result.ToList();\n\t\t_cacheHelper.SetCacheObject(CacheKeys.UserRole(userID), list);\n\t\treturn list;\n\t}\n\n\tprivate async Task AddUserToRole(int userID, string role)\n\t{\n\t\tawait RemoveUserFromRole(userID, role);\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_PopForumsUserRole (UserID, Role) VALUES (@UserID, @Role)\", new { UserID = userID, Role = role }));\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.UserRole(userID));\n\t}\n\n\tprivate async Task RemoveUserFromRole(int userID, string role)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_PopForumsUserRole WHERE UserID = @UserID AND Role = @Role\", new { UserID = userID, Role = role }));\n\t\t_cacheHelper.RemoveCacheObject(CacheKeys.UserRole(userID));\n\t}\n\n\tpublic async Task ReplaceUserRoles(int userID, string[] roles)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_PopForumsUserRole WHERE UserID = @UserID\", new { UserID = userID }));\n\t\tforeach (var role in roles)\n\t\t\tawait AddUserToRole(userID, role);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/SearchIndexQueueRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class SearchIndexQueueRepository : ISearchIndexQueueRepository\n{\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic SearchIndexQueueRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tpublic async Task Enqueue(SearchIndexPayload payload)\n\t{\n\t\tvar serializedPayload = JsonSerializer.Serialize(payload);\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_SearchQueue (Payload) VALUES (@Payload)\", new { Payload = serializedPayload }));\n\t}\n\n\tpublic async Task<SearchIndexPayload> Dequeue()\n\t{\n\t\tvar sql = @\"WITH cte AS (\nSELECT TOP(1) Payload\nFROM pf_SearchQueue WITH (ROWLOCK, READPAST)\nORDER BY Id)\nDELETE FROM cte\nOUTPUT DELETED.Payload;\";\n\t\tTask<string> serializedPayload = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tserializedPayload = connection.QuerySingleOrDefaultAsync<string>(sql));\n\t\tif (string.IsNullOrEmpty(serializedPayload.Result))\n\t\t\treturn new SearchIndexPayload();\n\t\tvar payload = JsonSerializer.Deserialize<SearchIndexPayload>(serializedPayload.Result);\n\t\treturn payload;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/SearchRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class SearchRepository : ISearchRepository\n{\n\tpublic SearchRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic virtual async Task<List<string>> GetJunkWords()\n\t{\n\t\tTask<IEnumerable<string>> words = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\twords = connection.QueryAsync<string>(\"SELECT JunkWord FROM pf_JunkWords ORDER BY JunkWord\"));\n\t\treturn words.Result.ToList();\n\t}\n\n\tpublic virtual async Task CreateJunkWord(string word)\n\t{\n\t\tTask<IEnumerable<string>> exists = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\texists = connection.QueryAsync<string>(\"SELECT JunkWord FROM pf_JunkWords WHERE JunkWord LIKE @JunkWord\", new { JunkWord = word }));\n\t\tif (exists.Result.Any())\n\t\t\treturn;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_JunkWords (JunkWord) VALUES (@JunkWord)\", new { JunkWord = word }));\n\t}\n\n\tpublic virtual async Task DeleteJunkWord(string word)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_JunkWords WHERE JunkWord = @JunkWord\", new { JunkWord = word }));\n\t}\n\n\tpublic virtual async Task DeleteAllIndexedWordsForTopic(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t{\n\t\t\tvar command = new SqlCommand(\"DELETE FROM pf_TopicSearchWords WHERE TopicID = @TopicID\", (SqlConnection)connection);\n\t\t\tcommand.CommandTimeout = 120;\n\t\t\tcommand.Parameters.Add(new SqlParameter(\"TopicID\", topicID));\n\t\t\treturn command.ExecuteNonQueryAsync();\n\t\t});\n\t}\n\n\tpublic virtual async Task SaveSearchWord(int topicID, string word, int rank)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_TopicSearchWords (SearchWord, TopicID, Rank) VALUES (@SearchWord, @TopicID, @Rank)\", new { SearchWord = word, TopicID = topicID, Rank = rank }));\n\t}\n\n\tpublic virtual async Task<Tuple<Response<List<Topic>>, int>> SearchTopics(string searchTerm, List<int> hiddenForums, SearchType searchType, int startRow, int pageSize)\n\t{\n\t\tvar topicCount = 0;\n\t\tif (searchTerm.Trim() == string.Empty)\n\t\t\treturn Tuple.Create(new Response<List<Topic>>(new List<Topic>()), topicCount);\n\t\tvar topics = new List<Topic>();\n\t\tvar wordArray = searchTerm.Split(new [] { ' ' });\n\t\tvar wordList = new List<string>();\n\t\tvar junkWords = await GetJunkWords();\n\t\tvar alphaNum = SearchService.SearchWordPattern;\n\t\tfor (var x = 0; x < wordArray.Length; x++)\n\t\t{\n\t\t\tforeach (Match match in alphaNum.Matches(wordArray[x]))\n\t\t\t{\n\t\t\t\tif (!junkWords.Contains(match.Value))\n\t\t\t\t\twordList.Add(match.Value);\n\t\t\t}\n\t\t}\n\t\tvar words = wordList.ToArray();\n\t\tvar wordParameters = new Dictionary<string, string>();\n\t\tvar sb = new StringBuilder();\n\t\tsb.Append(\"WITH FirstEntries AS (SELECT ROW_NUMBER() OVER (PARTITION BY pf_Topic.TopicID ORDER BY pf_Topic.LastPostTime DESC) AS GroupRow, \");\n\t\tsb.Append(TopicRepository.TopicFields);\n\t\tsb.Append(\", ((\");\n\t\tfor (var x = 0; x < words.Length; x++)\n\t\t{\n\t\t\tsb.Append(\"q\");\n\t\t\tsb.Append(x.ToString());\n\t\t\tsb.Append(\".Rank\");\n\t\t\tif (x < words.Length - 1) sb.Append(\"+\");\n\t\t}\n\t\tsb.Append(\")/\" + words.Length + \") AS CompositeRank FROM pf_Topic \");\n\t\tfor (int x = 0; x < words.Length; x++)\n\t\t{\n\t\t\tstring xstring = x.ToString();\n\t\t\tsb.Append(\" JOIN (SELECT TOP 10000 TopicID, pf_TopicSearchWords.Rank FROM pf_TopicSearchWords WHERE SearchWord = \");\n\t\t\tvar param = \"@w\" + x;\n\t\t\twordParameters.Add(param, words[x]);\n\t\t\tsb.Append(param);\n\t\t\tsb.Append(\" ORDER BY pf_TopicSearchWords.Rank DESC) AS q\");\n\t\t\tsb.Append(xstring);\n\t\t\tsb.Append(\" ON pf_Topic.TopicID = q\");\n\t\t\tsb.Append(xstring);\n\t\t\tsb.Append(\".TopicID\");\n\t\t}\n\t\tsb.Append(\" WHERE NOT pf_Topic.IsDeleted = 1\");\n\t\tif (hiddenForums.Count > 0)\n\t\t{\n\t\t\tsb.Append(\" AND\");\n\t\t\tfor (int x = 0; x < hiddenForums.Count; x++)\n\t\t\t{\n\t\t\t\tif (x > 0) sb.Append(\" AND\");\n\t\t\t\tsb.Append($\" NOT ForumID = {hiddenForums[x]} \");\n\t\t\t}\n\t\t}\n\n\t\tstring orderBy;\n\t\tswitch (searchType)\n\t\t{\n\t\t\tcase SearchType.Date:\n\t\t\t\torderBy = \"LastPostTime DESC\";\n\t\t\t\tbreak;\n\t\t\tcase SearchType.Name:\n\t\t\t\torderBy = \"StartedByName\";\n\t\t\t\tbreak;\n\t\t\tcase SearchType.Replies:\n\t\t\t\torderBy = \"ReplyCount DESC\";\n\t\t\t\tbreak;\n\t\t\tcase SearchType.Title:\n\t\t\t\torderBy = \"Title\";\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\torderBy = \"CompositeRank DESC, LastPostTime DESC\";\n\t\t\t\tbreak;\n\t\t}\n\n\t\tsb.Append(\"),\\r\\nEntries as (SELECT *,ROW_NUMBER() OVER (ORDER BY \");\n\t\tsb.Append(orderBy);\n\t\tsb.Append(\") AS Row, COUNT(*) OVER () as cnt FROM FirstEntries WHERE GroupRow = 1)\\r\\nSELECT TopicID, ForumID, Title, ReplyCount, ViewCount, StartedByUserID, StartedByName, LastPostUserID, LastPostName, LastPostTime, IsClosed, IsPinned, IsDeleted, UrlName, AnswerPostID, cnt FROM Entries WHERE Row BETWEEN @StartRow AND @StartRow + @PageSize - 1\");\n\n\t\tif (words.Length == 0)\n\t\t\treturn Tuple.Create(new Response<List<Topic>>(new List<Topic>()), topicCount);\n\n\t\tvar connection = _sqlObjectFactory.GetConnection();\n\t\tvar command = connection.Command(_sqlObjectFactory, sb.ToString());\n\t\tcommand.AddParameter(_sqlObjectFactory, \"@StartRow\", startRow);\n\t\tcommand.AddParameter(_sqlObjectFactory, \"@PageSize\", pageSize);\n\t\tforeach (var item in wordParameters)\n\t\t\tcommand.AddParameter(_sqlObjectFactory, item.Key, item.Value);\n\t\tconnection.Open();\n\t\tvar reader = await command.ExecuteReaderAsync();\n\n\t\twhile (reader.Read())\n\t\t{\n\t\t\tvar topic = new Topic\n\t\t\t{\n\t\t\t\tTopicID = reader.GetInt32(0),\n\t\t\t\tForumID = reader.GetInt32(1),\n\t\t\t\tTitle = reader.GetString(2),\n\t\t\t\tReplyCount = reader.GetInt32(3),\n\t\t\t\tViewCount = reader.GetInt32(4),\n\t\t\t\tStartedByUserID = reader.GetInt32(5),\n\t\t\t\tStartedByName = reader.GetString(6),\n\t\t\t\tLastPostUserID = reader.GetInt32(7),\n\t\t\t\tLastPostName = reader.GetString(8),\n\t\t\t\tLastPostTime = reader.GetDateTime(9),\n\t\t\t\tIsClosed = reader.GetBoolean(10),\n\t\t\t\tIsPinned = reader.GetBoolean(11),\n\t\t\t\tIsDeleted = reader.GetBoolean(12),\n\t\t\t\tUrlName = reader.GetString(13),\n\t\t\t\tAnswerPostID = reader.NullIntDbHelper(14)\n\t\t\t};\n\t\t\ttopics.Add(topic);\n\t\t\ttopicCount = Convert.ToInt32(reader[\"cnt\"]);\n\t\t}\n\t\treader.Dispose();\n\t\tconnection.Close();\n\t\t// simple response since results are from database, not external service like ES or Azure\n\t\treturn Tuple.Create(new Response<List<Topic>>(topics), topicCount);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/SecurityLogRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class SecurityLogRepository : ISecurityLogRepository\n{\n\tpublic SecurityLogRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task Create(SecurityLogEntry logEntry)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_SecurityLog (SecurityLogType, UserID, TargetUserID, IP, Message, ActivityDate) VALUES (@SecurityLogType, @UserID, @TargetUserID, @IP, @Message, @ActivityDate)\", new { logEntry.SecurityLogType, logEntry.UserID, logEntry.TargetUserID, logEntry.IP, logEntry.Message, logEntry.ActivityDate }));\n\t}\n\n\tpublic async Task<List<SecurityLogEntry>> GetByUserID(int userID, DateTime startDate, DateTime endDate)\n\t{\n\t\tTask<IEnumerable<SecurityLogEntry>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<SecurityLogEntry>(\"SELECT SecurityLogID, SecurityLogType, UserID, TargetUserID, IP, Message, ActivityDate FROM pf_SecurityLog WHERE (UserID = @UserID OR TargetUserID = @UserID) AND ActivityDate >= @StartDate AND ActivityDate <= @EndDate ORDER BY ActivityDate\", new { UserID = userID, StartDate = startDate, EndDate = endDate }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<List<IPHistoryEvent>> GetIPHistory(string ip, DateTime start, DateTime end)\n\t{\n\t\tvar list = new List<IPHistoryEvent>();\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(async connection =>\n\t\t{\n\t\t\tvar reader = await connection.ExecuteReaderAsync(\"SELECT SecurityLogID AS ID, ActivityDate AS EventTime, TargetUserID AS UserID, SecurityLogType, Message FROM pf_SecurityLog WHERE IP = @IP AND ActivityDate >= @Start AND ActivityDate <= @End ORDER BY ActivityDate\", new {IP = ip, Start = start, End = end});\n\t\t\twhile (reader.Read())\n\t\t\t{\n\t\t\t\tlist.Add(new IPHistoryEvent\n\t\t\t\t{\n\t\t\t\t\tID = reader.GetInt32(0),\n\t\t\t\t\tEventTime = reader.GetDateTime(1),\n\t\t\t\t\tUserID = reader.IsDBNull(2) ? (int?)null : reader.GetInt32(2),\n\t\t\t\t\tName = string.Empty,\n\t\t\t\t\tDescription = $\"{((SecurityLogType) reader[3]).ToString()} - {(reader.IsDBNull(4) ? null : reader.GetString(4))}\",\n\t\t\t\t\tType = \"SecurityLogEntry\"\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t\treturn list;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/ServiceHeartbeatRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class ServiceHeartbeatRepository : IServiceHeartbeatRepository\n{\n\tpublic ServiceHeartbeatRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\t\t\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task RecordHeartbeat(string serviceName, string machineName, DateTime lastRun)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(async connection =>\n\t\t{\n\t\t\tawait connection.ExecuteAsync(\"DELETE FROM pf_ServiceHeartbeat WHERE ServiceName = @ServiceName AND MachineName = @MachineName\", new { ServiceName = serviceName, MachineName = machineName });\n\t\t\tawait connection.ExecuteAsync(\"INSERT INTO pf_ServiceHeartbeat (ServiceName, MachineName, LastRun) VALUES (@ServiceName, @MachineName, @LastRun)\", new { ServiceName = serviceName, MachineName = machineName, LastRun = lastRun });\n\t\t});\n\t}\n\n\tpublic async Task<List<ServiceHeartbeat>> GetAll()\n\t{\n\t\tTask<IEnumerable<ServiceHeartbeat>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tlist = connection.QueryAsync<ServiceHeartbeat>(\"SELECT ServiceName, MachineName, LastRun FROM pf_ServiceHeartbeat ORDER BY ServiceName\"));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task ClearAll()\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_ServiceHeartbeat\"));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/SettingsRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class SettingsRepository : ISettingsRepository\n{\n\tpublic SettingsRepository(ISqlObjectFactory sqlObjectFactory, ICacheHelper cacheHelper)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t\t_cacheHelper = cacheHelper;\n\t\t_cacheHelper.OnRemoveCacheKey += key =>\n\t\t{\n\t\t\tvar effectiveCacheKey = _cacheHelper.GetEffectiveCacheKey(CacheKey);\n\t\t\tif (key == effectiveCacheKey)\n\t\t\t\tOnSettingsInvalidated?.Invoke();\n\t\t};\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\tprivate readonly ICacheHelper _cacheHelper;\n\tprivate const string CacheKey = \"pf.settings\";\n\n\tpublic event Action OnSettingsInvalidated;\n\n\tpublic Dictionary<string, string> Get()\n\t{\n\t\tvar cached = _cacheHelper.GetCacheObject<Dictionary<string, string>>(CacheKey);\n\t\tif (cached != null)\n\t\t\treturn cached;\n\t\tvar dictionary = new Dictionary<string, string>();\n\t\t_sqlObjectFactory.GetConnection().Using(connection =>\n\t\t\tdictionary = connection.Query(\"SELECT Setting, [Value] FROM pf_Setting\").ToDictionary(r => (string)r.Setting, r => (string)r.Value));\n\t\t_cacheHelper.SetCacheObject(CacheKey, dictionary);\n\t\treturn dictionary;\n\t}\n\n\tpublic void Save(Dictionary<string, object> dictionary)\n\t{\n\t\t_sqlObjectFactory.GetConnection().Using(connection =>\n\t\t{\n\t\t\tconnection.Execute(\"DELETE FROM pf_Setting\");\n\t\t\tforeach (var key in dictionary)\n\t\t\t\tconnection.Execute(\"INSERT INTO pf_Setting (Setting, [Value]) VALUES (@Setting, @Value)\", new { Setting = key.Key, Value = key.Value == null ? string.Empty : key.Value.ToString()});\n\t\t});\n\t\t_cacheHelper.RemoveCacheObject(CacheKey);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/SetupRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class SetupRepository : ISetupRepository\n{\n\tpublic SetupRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic bool IsConnectionPossible()\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar connection = _sqlObjectFactory.GetConnection();\n\t\t\tconnection.Open();\n\t\t\tconnection.Close();\n\t\t\treturn true;\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tpublic virtual bool IsDatabaseSetup()\n\t{\n\t\tconst string sql = @\"IF (NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'pf_PopForumsUser'))\nBEGIN\n    SELECT 0\nEND\nELSE\nBEGIN\n\tSELECT 1\nEND\";\n\t\tvar result = false;\n\t\t_sqlObjectFactory.GetConnection().Using(connection => \n\t\t\tresult = connection.ExecuteScalar<bool>(sql));\n\t\treturn result;\n\t}\n\n\tpublic void SetupDatabase()\n\t{\n\t\tvar assembly = typeof(SetupRepository).GetTypeInfo().Assembly;\n\t\tvar stream = assembly.GetManifestResourceStream(\"PopForums.Sql.PopForums.sql\");\n\t\tvar reader = new StreamReader(stream);\n\t\tvar sql = reader.ReadToEnd();\n\t\tusing var connection = _sqlObjectFactory.GetConnection() as SqlConnection;\n\t\tusing var command = new SqlCommand(sql, connection);\n\t\tconnection.Open();\n\t\tcommand.ExecuteNonQuery();\n\t\tconnection.Close();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/SubscribeNotificationRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class SubscribeNotificationRepository : ISubscribeNotificationRepository\n{\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic SubscribeNotificationRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tpublic async Task Enqueue(SubscribeNotificationPayload payload)\n\t{\n\t\tvar serializedPayload = JsonSerializer.Serialize(payload);\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_SubNotifyQueue (Payload) VALUES (@Payload)\", new { Payload = serializedPayload }));\n\t}\n\n\tpublic async Task<SubscribeNotificationPayload> Dequeue()\n\t{\n\t\tTask<string> serializedPayload = null;\n\t\tvar sql = @\"WITH cte AS (\nSELECT TOP(1) Payload\nFROM pf_SubNotifyQueue WITH (ROWLOCK, READPAST)\nORDER BY Id)\nDELETE FROM cte\nOUTPUT DELETED.Payload;\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tserializedPayload = connection.QuerySingleOrDefaultAsync<string>(sql));\n\t\tif (string.IsNullOrEmpty(serializedPayload.Result))\n\t\t\treturn null;\n\t\tvar payload = JsonSerializer.Deserialize<SubscribeNotificationPayload>(serializedPayload.Result);\n\t\treturn payload;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/SubscribedTopicsRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class SubscribedTopicsRepository : ISubscribedTopicsRepository\n{\n\tpublic SubscribedTopicsRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic async Task<List<Topic>> GetSubscribedTopics(int userID, int startRow, int pageSize)\n\t{\n\t\tTask<IEnumerable<Topic>> list = null;\n\t\tconst string sql = @\"\nDECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH Entries AS ( \nSELECT ROW_NUMBER() OVER (ORDER BY IsPinned DESC, LastPostTime DESC)\nAS Row, pf_Topic.TopicID, pf_Topic.ForumID, pf_Topic.Title, pf_Topic.ReplyCount, pf_Topic.ViewCount, \npf_Topic.StartedByUserID, pf_Topic.StartedByName, pf_Topic.LastPostUserID, pf_Topic.LastPostName, \npf_Topic.LastPostTime, pf_Topic.IsClosed, pf_Topic.IsPinned, pf_Topic.IsDeleted, pf_Topic.UrlName, pf_Topic.AnswerPostID \nFROM pf_Topic JOIN pf_SubscribeTopic S ON pf_Topic.TopicID = S.TopicID \nWHERE S.UserID = @UserID AND pf_Topic.IsDeleted = 0)\n\nSELECT TopicID, ForumID, Title, ReplyCount, ViewCount, \nStartedByUserID, StartedByName, LastPostUserID, LastPostName, \nLastPostTime, IsClosed, IsPinned, IsDeleted, UrlName, AnswerPostID\nFROM Entries \nWHERE Row between \n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<Topic>(sql, new { UserID = userID, StartRow = startRow, PageSize = pageSize}));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<int> GetSubscribedTopicCount(int userID)\n\t{\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(*) FROM pf_SubscribeTopic S JOIN pf_Topic T ON S.TopicID = T.TopicID WHERE S.UserID = @UserID AND T.IsDeleted = 0\", new { UserID = userID }));\n\t\treturn await count;\n\t}\n\n\tpublic async Task<List<int>> GetSubscribedUserIDs(int topicID)\n\t{\n\t\tTask<IEnumerable<int>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<int>(\"SELECT pf_SubscribeTopic.UserID FROM pf_PopForumsUser JOIN pf_SubscribeTopic ON pf_PopForumsUser.UserID = pf_SubscribeTopic.UserID WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<bool> IsTopicSubscribed(int userID, int topicID)\n\t{\n\t\tTask<IEnumerable<dynamic>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync(\"SELECT * FROM pf_SubscribeTopic WHERE UserID = @UserID AND TopicID = @TopicID\", new { UserID = userID, TopicID = topicID }));\n\t\treturn result.Result.Any();\n\t}\n\n\tpublic async Task AddSubscribedTopic(int userID, int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_SubscribeTopic (UserID, TopicID) VALUES (@UserID, @TopicID)\", new { UserID = userID, TopicID = topicID }));\n\t}\n\n\tpublic async Task RemoveSubscribedTopic(int userID, int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_SubscribeTopic WHERE UserID = @UserID AND TopicID = @TopicID\", new { UserID = userID, TopicID = topicID }));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/TopicRepository.cs",
    "content": "namespace PopForums.Sql.Repositories;\n\npublic class TopicRepository : ITopicRepository\n{\n\tpublic TopicRepository(ISqlObjectFactory sqlObjectFactory, ICacheHelper cache)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t\t_cache = cache;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\tprivate readonly ICacheHelper _cache;\n\tinternal const string TopicFields = \"pf_Topic.TopicID, pf_Topic.ForumID, pf_Topic.Title, pf_Topic.ReplyCount, pf_Topic.ViewCount, pf_Topic.StartedByUserID, pf_Topic.StartedByName, pf_Topic.LastPostUserID, pf_Topic.LastPostName, pf_Topic.LastPostTime, pf_Topic.IsClosed, pf_Topic.IsPinned, pf_Topic.IsDeleted, pf_Topic.UrlName, pf_Topic.AnswerPostID\";\n\n\tpublic async Task<Topic> GetLastUpdatedTopic(int forumID)\n\t{\n\t\tTask<Topic> topic = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttopic = connection.QuerySingleOrDefaultAsync<Topic>(\"SELECT TOP 1 \" + TopicFields + \n\t\t\t                                                    @\" FROM pf_Topic WHERE pf_Topic.ForumID = @ForumID AND pf_Topic.IsDeleted = 0 \nORDER BY pf_Topic.LastPostTime DESC\", new { ForumID = forumID }));\n\t\treturn await topic;\n\t}\n\n\tpublic async Task<int> GetTopicCount(int forumID, bool includeDelete)\n\t{\n\t\tvar sql = \"SELECT COUNT(*) FROM pf_Topic WHERE ForumID = @ForumID\";\n\t\tif (!includeDelete)\n\t\t\tsql += \" AND IsDeleted = 0\";\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tcount = connection.ExecuteScalarAsync<int>(sql, new { ForumID = forumID }));\n\t\treturn await count;\n\t}\n\n\tpublic async Task<int> GetTopicCountByUser(int userID, bool includeDeleted, List<int> excludedForums)\n\t{\n\t\tvar sql = \"SELECT COUNT(DISTINCT pf_Topic.TopicID) FROM pf_Topic JOIN pf_Post ON pf_Topic.TopicID = pf_Post.TopicID WHERE pf_Post.UserID = @UserID\";\n\t\tif (!includeDeleted)\n\t\t\tsql += \" AND pf_Topic.IsDeleted = 0 AND pf_Post.IsDeleted = 0\";\n\t\tsql = GenerateExcludedForumSql(sql, excludedForums);\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(sql, new { UserID = userID }));\n\t\treturn await count;\n\t}\n\n\tpublic async Task<int> GetTopicCount(bool includeDeleted, List<int> excludedForums)\n\t{\n\t\tvar sql = \"SELECT COUNT(*) FROM pf_Topic\";\n\t\tif (!includeDeleted)\n\t\t\tsql += \" WHERE IsDeleted = 0\";\n\t\telse\n\t\t\tsql += \" WHERE 1 = 1\";\n\t\tsql = GenerateExcludedForumSql(sql, excludedForums);\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(sql));\n\t\treturn await count;\n\t}\n\n\tprivate static string GenerateExcludedForumSql(string sql, List<int> excludedForums)\n\t{\n\t\tif (excludedForums.Count > 0)\n\t\t{\n\t\t\tsql += \" AND ForumID NOT IN (\";\n\t\t\tfor (var i = 0; i < excludedForums.Count; i++)\n\t\t\t{\n\t\t\t\tsql += excludedForums[i];\n\t\t\t\tif (i != excludedForums.Count - 1)\n\t\t\t\t\tsql += \",\";\n\t\t\t}\n\t\t\tsql += \")\";\n\t\t}\n\t\treturn sql;\n\t}\n\n\tpublic async Task<int> GetPostCount(int forumID, bool includeDelete)\n\t{\n\t\tvar sql = \"SELECT COUNT(pf_Post.TopicID) FROM pf_Post JOIN pf_Topic ON pf_Post.TopicID = pf_Topic.TopicID WHERE pf_Topic.ForumID = @ForumID\";\n\t\tif (!includeDelete)\n\t\t\tsql += \" AND pf_Post.IsDeleted = 0 AND pf_Topic.IsDeleted = 0\";\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(sql, new { ForumID = forumID }));\n\t\treturn await count;\n\t}\n\n\tpublic async Task<Topic> Get(int topicID)\n\t{\n\t\tTask<Topic> topic = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttopic = connection.QuerySingleOrDefaultAsync<Topic>(\"SELECT \" + TopicFields + \" FROM pf_Topic WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t\treturn await topic;\n\t}\n\n\tpublic async Task<Topic> Get(string urlName)\n\t{\n\t\tTask<Topic> topic = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttopic = connection.QuerySingleOrDefaultAsync<Topic>(\"SELECT \" + TopicFields + \" FROM pf_Topic WHERE UrlName = @UrlName\", new { UrlName = urlName }));\n\t\treturn await topic;\n\t}\n\n\tpublic async Task<List<Topic>> Get(int forumID, bool includeDeleted, int startRow, int pageSize)\n\t{\n\t\tconst string sql = @\"\nDECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH Entries AS ( \nSELECT ROW_NUMBER() OVER (ORDER BY IsPinned DESC, LastPostTime DESC)\nAS Row, pf_Topic.TopicID, pf_Topic.ForumID, pf_Topic.Title, pf_Topic.ReplyCount, pf_Topic.ViewCount, \npf_Topic.StartedByUserID, pf_Topic.StartedByName, pf_Topic.LastPostUserID, pf_Topic.LastPostName, \npf_Topic.LastPostTime, pf_Topic.IsClosed, pf_Topic.IsPinned, pf_Topic.IsDeleted, pf_Topic.UrlName, pf_Topic.AnswerPostID \nFROM pf_Topic WHERE ForumID = @ForumID \nAND ((@IncludeDeleted = 1) OR (@IncludeDeleted = 0 AND IsDeleted = 0)))\n\nSELECT TopicID, ForumID, Title, ReplyCount, ViewCount, \nStartedByUserID, StartedByName, LastPostUserID, LastPostName, \nLastPostTime, IsClosed, IsPinned, IsDeleted, UrlName, AnswerPostID\nFROM Entries \nWHERE Row between \n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tTask<IEnumerable<Topic>> topics = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\ttopics = connection.QueryAsync<Topic>(sql, new { ForumID = forumID, IncludeDeleted = includeDeleted, StartRow = startRow, PageSize = pageSize }));\n\t\treturn topics.Result.ToList();\n\t}\n\n\tpublic async Task<List<Topic>> GetTopicsByUser(int userID, bool includeDeleted, List<int> excludedForums, int startRow, int pageSize)\n\t{\n\t\tvar sql = @\"\nDECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH FirstEntries AS (\nSELECT ROW_NUMBER() OVER (PARTITION BY pf_Topic.TopicID ORDER BY IsPinned DESC, LastPostTime DESC)\nAS GroupRow, pf_Topic.TopicID, pf_Topic.ForumID, pf_Topic.Title, pf_Topic.ReplyCount, pf_Topic.ViewCount,\npf_Topic.StartedByUserID, pf_Topic.StartedByName, pf_Topic.LastPostUserID, pf_Topic.LastPostName,\npf_Topic.LastPostTime, pf_Topic.IsClosed, pf_Topic.IsPinned, pf_Topic.IsDeleted, pf_Topic.UrlName, pf_Topic.AnswerPostID\nFROM pf_Topic JOIN pf_Post ON pf_Topic.TopicID = pf_Post.TopicID\nWHERE pf_Post.UserID = @UserID AND ((@IncludeDeleted = 1) OR (@IncludeDeleted = 0 AND pf_Topic.IsDeleted = 0))\";\n\t\tsql = GenerateExcludedForumSql(sql, excludedForums);\n\t\tsql += @\"),\n\nEntries as (\nSELECT *,ROW_NUMBER() OVER (ORDER BY IsPinned DESC, LastPostTime DESC) AS Row\nFROM   FirstEntries\nWHERE  GroupRow = 1)\n\nSELECT TopicID, Entries.ForumID, Entries.Title, ReplyCount, ViewCount,\nStartedByUserID, StartedByName, LastPostUserID, Entries.LastPostName,\nEntries.LastPostTime, IsClosed, IsPinned, IsDeleted, UrlName, AnswerPostID\nFROM Entries \nWHERE Row between\n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tTask<IEnumerable<Topic>> topics = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttopics = connection.QueryAsync<Topic>(sql, new { UserID = userID, IncludeDeleted = includeDeleted, StartRow = startRow, PageSize = pageSize }));\n\t\treturn topics.Result.ToList();\n\t}\n\n\tpublic async Task<List<Topic>> Get(bool includeDeleted, List<int> excludedForums, int startRow, int pageSize)\n\t{\n\t\tvar sql = @\"\nDECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH Entries AS ( \nSELECT ROW_NUMBER() OVER (ORDER BY LastPostTime DESC)\nAS Row, pf_Topic.TopicID, pf_Topic.ForumID, pf_Topic.Title, pf_Topic.ReplyCount, pf_Topic.ViewCount, \npf_Topic.StartedByUserID, pf_Topic.StartedByName, pf_Topic.LastPostUserID, pf_Topic.LastPostName, \npf_Topic.LastPostTime, pf_Topic.IsClosed, pf_Topic.IsPinned, pf_Topic.IsDeleted, pf_Topic.UrlName, pf_Topic.AnswerPostID \nFROM pf_Topic WHERE ((@IncludeDeleted = 1) OR (@IncludeDeleted = 0 AND IsDeleted = 0))\";\n\t\tsql = GenerateExcludedForumSql(sql, excludedForums);\n\t\tsql += @\")\nSELECT TopicID, ForumID, Title, ReplyCount, ViewCount, \nStartedByUserID, StartedByName, LastPostUserID, LastPostName, \nLastPostTime, IsClosed, IsPinned, IsDeleted, UrlName, AnswerPostID\nFROM Entries \nWHERE Row between \n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tTask<IEnumerable<Topic>> topics = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttopics = connection.QueryAsync<Topic>(sql, new { IncludeDeleted = includeDeleted, StartRow = startRow, PageSize = pageSize }));\n\t\treturn topics.Result.ToList();\n\t}\n\n\tpublic async Task<List<Topic>> Get(IEnumerable<int> topicIDs)\n\t{\n\t\tvar list = topicIDs.ToList();\n\t\tif (list.Count == 0)\n\t\t\treturn new List<Topic>();\n\t\tvar ids = string.Join(\",\", list);\n\t\tvar sql = $@\"SELECT {TopicFields} FROM pf_Topic \nWHERE TopicID IN ({ids})\";\n\t\tvar count = list.Count;\n\t\tif (count > 1)\n\t\t{\n\t\t\tvar x = 1;\n\t\t\tvar orderBy = @\"\nORDER BY\nCASE\";\n\t\t\tforeach (var topicID in list.Take(count - 1))\n\t\t\t{\n\t\t\t\torderBy += $@\"\nWHEN TopicID = {topicID} THEN {x}\";\n\t\t\t\tx++;\n\t\t\t}\n\t\t\torderBy += $@\"\nELSE {x}\nEND\";\n\t\t\tsql += orderBy;\n\t\t}\n\t\tTask<IEnumerable<Topic>> topics = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttopics = connection.QueryAsync<Topic>(sql));\n\t\treturn topics.Result.ToList();\n\t}\n\n\tpublic async Task<List<Topic>> Get(int forumID, bool includeDeleted, List<int> excludedForums)\n\t{\n\t\tvar sql = @\"\nSELECT pf_Topic.TopicID, pf_Topic.ForumID, pf_Topic.Title, pf_Topic.ReplyCount, pf_Topic.ViewCount, \npf_Topic.StartedByUserID, pf_Topic.StartedByName, pf_Topic.LastPostUserID, pf_Topic.LastPostName, \npf_Topic.LastPostTime, pf_Topic.IsClosed, pf_Topic.IsPinned, pf_Topic.IsDeleted, pf_Topic.UrlName, pf_Topic.AnswerPostID \nFROM pf_Topic WHERE ForumID = @ForumID AND ((@IncludeDeleted = 1) OR (@IncludeDeleted = 0 AND IsDeleted = 0))\";\n\t\tsql = GenerateExcludedForumSql(sql, excludedForums);\n\t\tTask<IEnumerable<Topic>> topics = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttopics = connection.QueryAsync<Topic>(sql, new { ForumID = forumID, IncludeDeleted = includeDeleted }));\n\t\treturn topics.Result.ToList();\n\t}\n\n\tpublic async Task<List<string>> GetUrlNamesThatStartWith(string urlName)\n\t{\n\t\tTask<IEnumerable<string>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<string>(\"SELECT UrlName FROM pf_Topic WHERE UrlName LIKE @UrlName + '%'\", new { UrlName = urlName }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic virtual async Task<int> Create(int forumID, string title, int replyCount, int viewCount, int startedByUserID, string startedByName, int lastPostUserID, string lastPostName, DateTime lastPostTime, bool isClosed, bool isPinned, bool isDeleted, string urlName)\n\t{\n\t\tTask<int> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QuerySingleAsync<int>(\"INSERT INTO pf_Topic (ForumID, Title, ReplyCount, ViewCount, StartedByUserID, StartedByName, LastPostUserID, LastPostName, LastPostTime, IsClosed, IsPinned, IsDeleted, UrlName) VALUES (@ForumID, @Title, @ReplyCount, @ViewCount, @StartedByUserID, @StartedByName, @LastPostUserID, @LastPostName, @LastPostTime, @IsClosed, @IsPinned, @IsDeleted, @UrlName);SELECT CAST(SCOPE_IDENTITY() as int)\", new { ForumID = forumID, Title = title, ReplyCount = replyCount, ViewCount = viewCount, StartedByUserID = startedByUserID, StartedByName = startedByName, LastPostUserID = lastPostUserID, LastPostName = lastPostName, LastPostTime = lastPostTime, IsClosed = isClosed, IsPinned = isPinned, IsDeleted = isDeleted, UrlName = urlName }));\n\t\treturn await result;\n\t}\n\n\tpublic async Task IncrementReplyCount(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET ReplyCount = ReplyCount + 1 WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t}\n\n\tpublic async Task IncrementViewCount(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET ViewCount = ViewCount + 1 WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t}\n\n\tpublic async Task UpdateLastTimeAndUser(int topicID, int userID, string name, DateTime postTime)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET LastPostUserID = @LastPostUserID, LastPostName = @LastPostName, LastPostTime = @LastPostTime WHERE TopicID = @TopicID\", new { LastPostUserID = userID, LastPostName = name, LastPostTime = postTime, TopicID = topicID }));\n\t}\n\n\tpublic async Task CloseTopic(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET IsClosed = 1 WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t}\n\n\tpublic async Task OpenTopic(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET IsClosed = 0 WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t}\n\n\tpublic async Task PinTopic(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET IsPinned = 1 WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t}\n\n\tpublic async Task UnpinTopic(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET IsPinned = 0 WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t}\n\n\tpublic async Task DeleteTopic(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET IsDeleted = 1 WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t\tvar key = string.Format(PostRepository.CacheKeys.PostPages, topicID);\n\t\t_cache.RemoveCacheObject(key);\n\t}\n\n\tpublic async Task UndeleteTopic(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET IsDeleted = 0 WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t}\n\n\tpublic async Task HardDeleteTopic(int topicID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_Topic WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t\tvar key = string.Format(PostRepository.CacheKeys.PostPages, topicID);\n\t\t_cache.RemoveCacheObject(key);\n\t}\n\n\tpublic async Task UpdateTitleAndForum(int topicID, int forumID, string newTitle, string newUrlName)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET ForumID = @ForumID, Title = @Title, UrlName = @UrlName WHERE TopicID = @TopicID\" ,new { ForumID = forumID, Title = newTitle, TopicID = topicID, UrlName = newUrlName}));\n\t}\n\n\tpublic async Task UpdateReplyCount(int topicID, int replyCount)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET ReplyCount = @ReplyCount WHERE TopicID = @TopicID\", new { ReplyCount = replyCount, TopicID = topicID }));\n\t}\n\n\tpublic async Task<DateTime?> GetLastPostTime(int topicID)\n\t{\n\t\tTask<DateTime> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.ExecuteScalarAsync<DateTime>(\"SELECT LastPostTime FROM pf_Topic WHERE TopicID = @TopicID\", new { TopicID = topicID }));\n\t\treturn await result;\n\t}\n\n\tpublic async Task UpdateAnswerPostID(int topicID, int? postID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_Topic SET AnswerPostID = @AnswerPostID WHERE TopicID = @TopicID\", new { AnswerPostID = postID, TopicID = topicID }));\n\t}\n\n\tpublic async Task<IEnumerable<int>> CloseTopicsOlderThan(DateTime cutoffDate)\n\t{\n\t\tconst string sql = @\"DECLARE @ids TABLE (id int)\nUPDATE pf_Topic SET IsClosed = 1 \nOUTPUT INSERTED.TopicID\nWHERE LastPostTime < @CutoffDate AND IsClosed = 0\nSELECT id FROM @ids\";\n\t\tTask<IEnumerable<int>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<int>(sql, new { CutoffDate = cutoffDate }));\n\t\treturn await list;\n\t}\n\n\tpublic async Task<List<Tuple<string, DateTime>>> GetUrlNames(bool includeDeleted, List<int> excludedForums, int startRow, int pageSize)\n\t{\n\t\tvar sql = @\"\nDECLARE @Counter int\nSET @Counter = (@StartRow + @PageSize - 1)\n\nSET ROWCOUNT @Counter;\n\nWITH Entries AS ( \nSELECT ROW_NUMBER() OVER (ORDER BY LastPostTime DESC)\nAS Row, pf_Topic.UrlName, pf_Topic.LastPostTime \nFROM pf_Topic WHERE ((@IncludeDeleted = 1) OR (@IncludeDeleted = 0 AND IsDeleted = 0))\";\n\t\tsql = GenerateExcludedForumSql(sql, excludedForums);\n\t\tsql += @\")\nSELECT UrlName AS Item1, LastPostTime AS Item2\nFROM Entries \nWHERE Row between \n@StartRow and @StartRow + @PageSize - 1\n\nSET ROWCOUNT 0\";\n\t\tTask<IEnumerable<Tuple<string, DateTime>>> topics = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttopics = connection.QueryAsync<Tuple<string, DateTime>>(sql, new { IncludeDeleted = includeDeleted, StartRow = startRow, PageSize = pageSize }));\n\t\treturn topics.Result.ToList();\n\t}\n\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/TopicViewLogRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class TopicViewLogRepository : ITopicViewLogRepository\n{\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic TopicViewLogRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tpublic async Task Log(int? userID, int topicID, DateTime timeStamp)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_TopicViewLog (UserID, TopicID, [TimeStamp]) VALUES (@UserID, @TopicID, @TimeStamp)\", new {UserID = userID, TopicID = topicID, TimeStamp = timeStamp}));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/UserAvatarRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class UserAvatarRepository : IUserAvatarRepository\n{\n\tpublic UserAvatarRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\t[Obsolete(\"Use GetImageStream(int) instead.\")]\n\tpublic async Task<byte[]> GetImageData(int userAvatarID)\n\t{\n\t\tTask<byte[]> data = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tdata = connection.ExecuteScalarAsync<byte[]>(\"SELECT ImageData FROM pf_UserAvatar WHERE UserAvatarID = @UserAvatarID\", new { UserAvatarID = userAvatarID }));\n\t\treturn await data;\n\t}\n\n\tpublic async Task<IStreamResponse> GetImageStream(int userAvatarID)\n\t{\n\t\tvar connection = (SqlConnection)_sqlObjectFactory.GetConnection();\n\t\tvar command = new SqlCommand(\"SELECT ImageData FROM pf_UserAvatar WHERE UserAvatarID = @UserAvatarID\", connection);\n\t\tcommand.Parameters.AddWithValue(\"@UserAvatarID\", userAvatarID);\n\t\tconnection.Open();\n\t\tvar reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess);\n\t\tif (await reader.ReadAsync() && !await reader.IsDBNullAsync(0))\n\t\t{\n\t\t\tvar stream = reader.GetStream(0);\n\t\t\tvar streamResponse = new StreamResponse(stream, connection, reader);\n\t\t\treturn streamResponse;\n\t\t}\n\t\treturn default;\n\t}\n\n\tpublic async Task<List<int>> GetUserAvatarIDs(int userID)\n\t{\n\t\tTask<IEnumerable<int>> ids = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tids = connection.QueryAsync<int>(\"SELECT UserAvatarID FROM pf_UserAvatar WHERE UserID = @UserID\",\n\t\t\t\tnew {UserID = userID}));\n\t\treturn ids.Result.ToList();\n\t}\n\n\tpublic async Task<int> SaveNewAvatar(int userID, byte[] imageData, DateTime timeStamp)\n\t{\n\t\tTask<int> avatarID = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tavatarID = connection.QuerySingleAsync<int>(\"INSERT INTO pf_UserAvatar (UserID, ImageData, [TimeStamp]) VALUES (@UserID, @ImageData, @TimeStamp);SELECT CAST(SCOPE_IDENTITY() as int)\", new { UserID = userID, TimeStamp = timeStamp, ImageData = imageData }));\n\t\treturn await avatarID;\n\t}\n\n\tpublic async Task DeleteAvatarsByUserID(int userID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_UserAvatar WHERE UserID = @UserID\", new { UserID = userID }));\n\t}\n\n\tpublic async Task<DateTime?> GetLastModificationDate(int userAvatarID)\n\t{\n\t\tTask<DateTime?> timeStamp = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttimeStamp = connection.QuerySingleOrDefaultAsync<DateTime?>(\"SELECT [TimeStamp] FROM pf_UserAvatar WHERE UserAvatarID = @UserAvatarID\", new { UserAvatarID = userAvatarID }));\n\t\treturn await timeStamp;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/UserAwardRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class UserAwardRepository : IUserAwardRepository\n{\n\tpublic UserAwardRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic virtual async Task IssueAward(int userID, string awardDefinitionID, string title, string description, DateTime timeStamp)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(@\"MERGE pf_UserAward WITH (HOLDLOCK) AS target\nUSING (SELECT @UserID, @AwardDefinitionID) AS source (UserID, AwardDefinitionID)\nON (target.UserID = source.UserID AND target.AwardDefinitionID = source.AwardDefinitionID)\nWHEN NOT MATCHED THEN\nINSERT (UserID, AwardDefinitionID, Title, Description, TimeStamp) VALUES (@UserID, @AwardDefinitionID, @Title, @Description, @TimeStamp);\", new { UserID = userID, AwardDefinitionID = awardDefinitionID, Title = title, Description = description, TimeStamp = timeStamp }));\n\t}\n\n\tpublic async Task<bool> IsAwarded(int userID, string awardDefinitionID)\n\t{\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(*) FROM pf_UserAward WHERE UserID = @UserID AND AwardDefinitionID = @AwardDefinitionID\", new { UserID = userID, AwardDefinitionID = awardDefinitionID }));\n\t\treturn count.Result > 0;\n\t}\n\n\tpublic async Task<List<UserAward>> GetAwards(int userID)\n\t{\n\t\tTask<IEnumerable<UserAward>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<UserAward>(\"SELECT UserAwardID, UserID, AwardDefinitionID, Title, Description, TimeStamp FROM pf_UserAward WHERE UserID = @UserID\", new { UserID = userID }));\n\t\treturn list.Result.ToList();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/UserImageRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class UserImageRepository : IUserImageRepository\n{\n\tpublic UserImageRepository(ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\t[Obsolete(\"Use GetImageStream(int) instead.\")]\n\tpublic async Task<byte[]> GetImageData(int userImageID)\n\t{\n\t\tTask<byte[]> data = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tdata = connection.ExecuteScalarAsync<byte[]>(\"SELECT ImageData FROM pf_UserImages WHERE UserImageID = @UserImageID\", new { UserImageID = userImageID }));\n\t\treturn await data;\n\t}\n\n\tpublic async Task<IStreamResponse> GetImageStream(int userImageID)\n\t{\n\t\tvar connection = (SqlConnection)_sqlObjectFactory.GetConnection();\n\t\tvar command = new SqlCommand(\"SELECT ImageData FROM pf_UserImages WHERE UserImageID = @UserImageID\", connection);\n\t\tcommand.Parameters.AddWithValue(\"UserImageID\", userImageID);\n\t\tconnection.Open();\n\t\tvar reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess);\n\t\tif (await reader.ReadAsync() && !await reader.IsDBNullAsync(0))\n\t\t{\n\t\t\tvar stream = reader.GetStream(0);\n\t\t\tvar streamResponse = new StreamResponse(stream, connection, reader);\n\t\t\treturn streamResponse;\n\t\t}\n\t\treturn default;\n\t}\n\n\tpublic async Task<List<UserImage>> GetUserImages(int userID)\n\t{\n\t\tTask<IEnumerable<UserImage>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<UserImage>(\"SELECT UserImageID, UserID, SortOrder, IsApproved FROM pf_UserImages WHERE UserID = @UserID ORDER BY SortOrder\", new { UserID = userID }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<int> SaveNewImage(int userID, int sortOrder, bool isApproved, byte[] imageData, DateTime timeStamp)\n\t{\n\t\tTask<int> userImageID = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tuserImageID = connection.QuerySingleAsync<int>(\"INSERT INTO pf_UserImages (UserID, SortOrder, IsApproved, [TimeStamp], ImageData) VALUES (@UserID, @SortOrder, @IsApproved, @TimeStamp, @ImageData);SELECT CAST(SCOPE_IDENTITY() as int)\", new { UserID = userID, SortOrder = sortOrder, IsApproved = isApproved, TimeStamp = timeStamp, ImageData = imageData }));\n\t\treturn await userImageID;\n\t}\n\n\tpublic async Task DeleteImagesByUserID(int userID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_UserImages WHERE UserID = @UserID\", new { UserID = userID }));\n\t}\n\n\tpublic async Task<DateTime?> GetLastModificationDate(int userImageID)\n\t{\n\t\tTask<DateTime?> timeStamp = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\ttimeStamp = connection.QuerySingleOrDefaultAsync<DateTime?>(\"SELECT [TimeStamp] FROM pf_UserImages WHERE UserImageID = @UserImageID\", new { UserImageID = userImageID }));\n\t\treturn await timeStamp;\n\t}\n\n\tpublic async Task<List<UserImage>> GetUnapprovedUserImages()\n\t{\n\t\tTask<IEnumerable<UserImage>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<UserImage>(\"SELECT UserImageID, UserID, SortOrder, IsApproved FROM pf_UserImages WHERE IsApproved = 0\"));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<bool?> IsUserImageApproved(int userImageID)\n\t{\n\t\tTask<bool?> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QuerySingleOrDefaultAsync<bool?>(\"SELECT IsApproved FROM pf_UserImages WHERE UserImageID = @UserImageID\", new { UserImageID = userImageID }));\n\t\treturn await result;\n\t}\n\n\tpublic async Task ApproveUserImage(int userImageID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_UserImages SET IsApproved = 1 WHERE UserImageID = @UserImageID\", new { UserImageID = userImageID }));\n\t}\n\n\tpublic async Task DeleteUserImage(int userImageID)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_UserImages WHERE UserImageID = @UserImageID\", new { UserImageID = userImageID }));\n\t}\n\n\tpublic async Task<UserImage> Get(int userImageID)\n\t{\n\t\tTask<UserImage> userImage = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tuserImage = connection.QuerySingleOrDefaultAsync<UserImage>(\"SELECT UserImageID, UserID, SortOrder, IsApproved FROM pf_UserImages WHERE UserImageID = @UserImageID\", new { UserImageID = userImageID }));\n\t\treturn await userImage;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/UserRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class UserRepository : IUserRepository\n{\n\tpublic UserRepository(ICacheHelper cacheHelper, ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_cacheHelper = cacheHelper;\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ICacheHelper _cacheHelper;\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic class CacheKeys\n\t{\n\t\tpublic const string UsersOnline = \"PopForums.Users.Online\";\n\t\tpublic const string TotalUsers = \"PopForums.Users.Total\";\n\t\tpublic const string PointTotals = \"PopForums.Users.Points.\";\n\t}\n\n\tpublic const string PopForumsUserColumns = \"pf_PopForumsUser.UserID, pf_PopForumsUser.Name, pf_PopForumsUser.Email, pf_PopForumsUser.CreationDate, pf_PopForumsUser.IsApproved, pf_PopForumsUser.AuthorizationKey, pf_PopForumsUser.TokenExpiration\";\n\n\tprivate void CacheUser(User user)\n\t{\n\t\t// We only cache users by name because it's the only consistent and repetitive get, looked up via the identity principal.\n\t\tvar key = \"PopForums.User.\" + user.Name;\n\t\t_cacheHelper.SetCacheObject(key, user);\n\t}\n\n\tprivate void RemoveCacheUser(string name)\n\t{\n\t\tvar key = \"PopForums.User.\" + name;\n\t\t_cacheHelper.RemoveCacheObject(key);\n\t}\n\n\tprivate User GetCachedUserByName(string name)\n\t{\n\t\tvar key = \"PopForums.User.\" + name;\n\t\treturn _cacheHelper.GetCacheObject<User>(key);\n\t}\n\n\tpublic async Task SetHashedPassword(User user, string hashedPassword, Guid salt)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PopForumsUser SET Password = @Password, Salt = @Salt WHERE UserID = @UserID\", new { Password = hashedPassword, Salt = salt, user.UserID }));\n\t}\n\n\tpublic async Task<Tuple<string, Guid?>> GetHashedPasswordByEmail(string email)\n\t{\n\t\tstring hashedPassword = null;\n\t\tGuid? saltCheck = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(async connection =>\n\t\t{\n\t\t\tvar reader = await connection.ExecuteReaderAsync(\"SELECT Password, Salt FROM pf_PopForumsUser WHERE Email = @Email\", new {Email = email});\n\t\t\tif (reader.Read())\n\t\t\t{\n\t\t\t\thashedPassword = reader.GetString(0);\n\t\t\t\tsaltCheck = reader.NullGuidDbHelper(1);\n\t\t\t}\n\t\t});\n\t\treturn Tuple.Create(hashedPassword, saltCheck);\n\t}\n\n\tpublic async Task<List<User>> GetUsersFromIDs(IList<int> ids)\n\t{\n\t\tTask<IEnumerable<User>> list = null;\n\t\tif (!ids.Any())\n\t\t\treturn new List<User>();\n\t\tvar sql = \"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser WHERE UserID IN (\" + ids[0];\n\t\tforeach (var id in ids.Skip(1))\n\t\t\tsql += \", \" + id;\n\t\tsql += \")\";\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<User>(sql));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<int> GetTotalUsers()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<int?>(CacheKeys.TotalUsers);\n\t\tif (cacheObject.HasValue)\n\t\t\treturn cacheObject.Value;\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(UserID) FROM pf_PopForumsUser\"));\n\t\t_cacheHelper.SetCacheObject(CacheKeys.TotalUsers, count.Result);\n\t\treturn await count;\n\t}\n\n\tprivate async Task<User> GetUser(string sql, object parameters)\n\t{\n\t\tTask<User> user = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tuser = connection.QuerySingleOrDefaultAsync<User>(sql, parameters));\n\t\treturn await user;\n\t}\n\n\tpublic async Task<User> GetUser(int userID)\n\t{\n\t\treturn await GetUser(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser WHERE UserID = @UserID\", new { UserID = userID });\n\t}\n\n\tpublic async Task<User> GetUserByName(string name)\n\t{\n\t\tvar cachedUser = GetCachedUserByName(name);\n\t\tif (cachedUser != null)\n\t\t\treturn cachedUser;\n\t\tvar user = await GetUser(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser WHERE Name = @Name\", new { Name = name });\n\t\tif (user == null)\n\t\t\treturn null;\n\t\tCacheUser(user);\n\t\treturn user;\n\t}\n\n\tpublic async Task<User> GetUserByEmail(string email)\n\t{\n\t\treturn await GetUser(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser WHERE Email = @Email\", new { Email = email });\n\t}\n\n\tpublic async Task<User> GetUserByAuthorizationKey(Guid key)\n\t{\n\t\treturn await GetUser(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser WHERE AuthorizationKey = @AuthorizationKey\", new { AuthorizationKey = key });\n\t}\n\n\tpublic virtual async Task<User> CreateUser(string name, string email, DateTime creationDate, bool isApproved, string hashedPassword, Guid authorizationKey, Guid salt)\n\t{\n\t\tTask<int> userID = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tuserID = connection.QuerySingleAsync<int>(\"INSERT INTO pf_PopForumsUser (Name, Email, CreationDate, IsApproved, AuthorizationKey, Password, Salt) VALUES (@Name, @Email, @CreationDate, @IsApproved, @AuthorizationKey, @Password, @Salt);SELECT CAST(SCOPE_IDENTITY() as int)\", new { Name = name, Email = email, CreationDate = creationDate, IsApproved = isApproved, AuthorizationKey = authorizationKey, Password = hashedPassword, Salt = salt }));\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_UserActivity (UserID, LastActivityDate, LastLoginDate) VALUES (@UserID, @LastActivityDate, @LastLoginDate)\", new {UserID = userID.Result, LastActivityDate = creationDate, LastLoginDate = creationDate}));\n\t\treturn new User {UserID = await userID, Name = name, Email = email, CreationDate = creationDate, IsApproved = isApproved, AuthorizationKey = authorizationKey};\n\t}\n\n\tpublic async Task UpdateLastActivityDate(User user, DateTime newDate)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_UserActivity SET LastActivityDate = @LastActivityDate WHERE UserID = @UserID\", new { LastActivityDate = newDate, user.UserID }));\n\t}\n\n\tpublic async Task UpdateLastLoginDate(User user, DateTime newDate)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_UserActivity SET LastLoginDate = @LastLoginDate WHERE UserID = @UserID\", new { LastLoginDate = newDate, user.UserID }));\n\t}\n\n\tpublic async Task UpdateRefreshToken(User user, string refreshToken)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_UserActivity SET RefreshToken = @refreshToken WHERE UserID = @UserID\", new { refreshToken, user.UserID }));\n\t}\n\n\tpublic async Task<string> GetRefreshToken(User user)\n\t{\n\t\tTask<string> refreshToken = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\trefreshToken = connection.QuerySingleOrDefaultAsync<string>(\"SELECT RefreshToken FROM pf_UserActivity WHERE UserID = @UserID\", new { user.UserID }));\n\t\treturn refreshToken.Result;\n\t}\n\n\tpublic async Task ChangeName(User user, string newName)\n\t{\n\t\tvar oldName = user.Name;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PopForumsUser SET Name = @Name WHERE UserID = @UserID\", new { Name = newName, user.UserID }));\n\t\tRemoveCacheUser(oldName);\n\t}\n\n\tpublic async Task ChangeEmail(User user, string newEmail)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection => \n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PopForumsUser SET Email = @Email WHERE UserID = @UserID\", new { Email = newEmail, user.UserID }));\n\t\tRemoveCacheUser(user.Name);\n\t}\n\n\tpublic async Task UpdateIsApproved(User user, bool isApproved)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PopForumsUser SET IsApproved = @IsApproved WHERE UserID = @UserID\", new { IsApproved = isApproved, user.UserID }));\n\t\tRemoveCacheUser(user.Name);\n\t}\n\n\tpublic async Task UpdateAuthorizationKey(User user, Guid key)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PopForumsUser SET AuthorizationKey = @AuthorizationKey WHERE UserID = @UserID\", new { AuthorizationKey = key, user.UserID }));\n\t\tRemoveCacheUser(user.Name);\n\t}\n\n\tpublic async Task UpdateTokenExpiration(User user, DateTime? tokenExpiration)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"UPDATE pf_PopForumsUser SET TokenExpiration = @tokenExpiration WHERE UserID = @UserID\", new { tokenExpiration, user.UserID }));\n\t\tRemoveCacheUser(user.Name);\n\t}\n\n\tpublic async Task<List<User>> SearchByEmail(string email)\n\t{\n\t\tvar list = await GetList(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser WHERE Email LIKE '%' + @Email + '%'\", new { Email = email });\n\t\treturn list;\n\t}\n\n\tpublic async Task<List<User>> SearchByName(string name)\n\t{\n\t\tvar list = await GetList(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser WHERE Name LIKE '%' + @Name + '%'\", new { Name = name });\n\t\treturn list;\n\t}\n\n\tpublic async Task<List<User>> SearchByRole(string role)\n\t{\n\t\tvar list = await GetList(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser JOIN pf_PopForumsUserRole R ON pf_PopForumsUser.UserID = R.UserID WHERE Role = @Role\", new { Role = role });\n\t\treturn list;\n\t}\n\n\tpublic async Task<List<User>> GetUsersOnline()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<List<User>>(CacheKeys.UsersOnline);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject;\n\t\tTask<IEnumerable<User>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<User>(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser JOIN pf_UserSession ON pf_PopForumsUser.UserID = pf_UserSession.UserID ORDER BY Name\"));\n\t\tvar userList = list.Result.ToList();\n\t\t_cacheHelper.SetCacheObject(CacheKeys.UsersOnline, userList, 60);\n\t\treturn userList;\n\t}\n\n\tpublic async Task<List<User>> GetSubscribedUsers()\n\t{\n\t\tTask<IEnumerable<User>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<User>(\"SELECT \" + PopForumsUserColumns + \" FROM pf_PopForumsUser JOIN pf_Profile ON pf_PopForumsUser.UserID = pf_Profile.UserID WHERE pf_Profile.IsSubscribed = 1\"));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic Dictionary<int, (User, int)> GetUsersByPointTotals(int top)\n\t{\n\t\tvar key = CacheKeys.PointTotals + top;\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<Dictionary<int, (User, int)>>(key);\n\t\tif (cacheObject != null)\n\t\t\treturn cacheObject;\n\t\tvar list = new Dictionary<int, (User, int)>();\n\t\t_sqlObjectFactory.GetConnection().Using(connection =>\n\t\t\tconnection.Query<User, int, User>(\n\t\t\t\t$\"SELECT TOP {top} {PopForumsUserColumns}, pf_Profile.Points FROM pf_PopForumsUser JOIN pf_Profile ON pf_PopForumsUser.UserID = pf_Profile.UserID ORDER BY pf_Profile.Points DESC\",\n\t\t\t\t(user, points) =>\n\t\t\t\t{\n\t\t\t\t\tlist.Add(user.UserID, (user, points));\n\t\t\t\t\treturn user;\n\t\t\t\t}, splitOn: \"Points\"));\n\t\t_cacheHelper.SetCacheObject(key, list, 60);\n\t\treturn list;\n\t}\n\n\tpublic async Task DeleteUser(User user)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_PopForumsUser WHERE UserID = @UserID\", new { user.UserID }));\n\t\tRemoveCacheUser(user.Name);\n\t}\n\n\tprivate async Task<List<User>> GetList(string sql, object parameters)\n\t{\n\t\tif (parameters == null)\n\t\t\treturn new List<User>();\n\t\tTask<IEnumerable<User>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<User>(sql, parameters));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<List<UserResult>> GetRecentUsers()\n\t{\n\t\tTask<IEnumerable<UserResult>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<UserResult>(@\"SELECT TOP 100 U.UserID, [Name], Email, CreationDate, [IP] \nFROM pf_PopForumsUser U JOIN pf_SecurityLog S ON U.UserID = S.TargetUserID \nWHERE SecurityLogType = 6 ORDER BY CreationDate DESC\"));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<IEnumerable<string>> GetUserNamesThatStartWith(string startingName)\n\t{\n\t\tTask<IEnumerable<string>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<string>(@\"SELECT [Name] FROM pf_PopForumsUser WHERE [Name] LIKE @startingName + '%'\", new { startingName }));\n\t\treturn list.Result.ToList();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/Repositories/UserSessionRepository.cs",
    "content": "﻿namespace PopForums.Sql.Repositories;\n\npublic class UserSessionRepository : IUserSessionRepository\n{\n\tpublic UserSessionRepository(ICacheHelper cacheHelper, ISqlObjectFactory sqlObjectFactory)\n\t{\n\t\t_cacheHelper = cacheHelper;\n\t\t_sqlObjectFactory = sqlObjectFactory;\n\t}\n\n\tprivate readonly ICacheHelper _cacheHelper;\n\tprivate readonly ISqlObjectFactory _sqlObjectFactory;\n\n\tpublic class CacheKeys\n\t{\n\t\tpublic const string CurrentSessionCount = \"PopForums.Session.CurrentCount\";\n\t}\n\n\tpublic async Task CreateSession(int sessionID, int? userID, DateTime lastTime)\n\t{\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"INSERT INTO pf_UserSession (SessionID, UserID, LastTime) VALUES (@SessionID, @UserID, @LastTime)\", new { SessionID = sessionID, UserID = userID, LastTime = lastTime }));\n\t}\n\n\tpublic async Task<bool> UpdateSession(int sessionID, DateTime lastTime)\n\t{\n\t\tTask<int> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.ExecuteAsync(\"UPDATE pf_UserSession SET LastTime = @LastTime WHERE SessionID = @SessionID\", new { SessionID = sessionID, LastTime = lastTime }));\n\t\treturn result.Result == 1;\n\t}\n\n\tpublic async Task<bool> IsSessionAnonymous(int sessionID)\n\t{\n\t\tTask<IEnumerable<int?>> result = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tresult = connection.QueryAsync<int?>(\"SELECT UserID FROM pf_UserSession WHERE SessionID = @SessionID AND UserID IS NULL\", new { SessionID = sessionID }));\n\t\treturn result.Result.Any();\n\t}\n\n\tpublic async Task<List<ExpiredUserSession>> GetAndDeleteExpiredSessions(DateTime cutOffDate)\n\t{\n\t\tTask<IEnumerable<ExpiredUserSession>> list = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tlist = connection.QueryAsync<ExpiredUserSession>(\"SELECT SessionID, UserID, LastTime FROM pf_UserSession WHERE LastTime < @CutOff\", new { CutOff = cutOffDate }));\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_UserSession WHERE LastTime < @CutOff\", new { CutOff = cutOffDate }));\n\t\treturn list.Result.ToList();\n\t}\n\n\tpublic async Task<ExpiredUserSession> GetSessionIDByUserID(int userID)\n\t{\n\t\tTask<ExpiredUserSession> session = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tsession = connection.QueryFirstOrDefaultAsync<ExpiredUserSession>(\"SELECT SessionID, UserID, LastTime FROM pf_UserSession WHERE UserID = @UserID\", new { UserID = userID }));\n\t\treturn await session;\n\t}\n\n\tpublic async Task DeleteSessions(int? userID, int sessionID)\n\t{\n\t\tif (userID.HasValue)\n\t\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_UserSession WHERE SessionID = @SessionID OR UserID = @UserID\", new { SessionID = sessionID, UserID = userID }));\n\t\telse\n\t\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\t\tconnection.ExecuteAsync(\"DELETE FROM pf_UserSession WHERE SessionID = @SessionID\", new { SessionID = sessionID }));\n\t}\n\n\tpublic async Task<int> GetTotalSessionCount()\n\t{\n\t\tvar cacheObject = _cacheHelper.GetCacheObject<int>(CacheKeys.CurrentSessionCount);\n\t\tif (cacheObject != 0)\n\t\t\treturn cacheObject;\n\t\tTask<int> count = null;\n\t\tawait _sqlObjectFactory.GetConnection().UsingAsync(connection =>\n\t\t\tcount = connection.ExecuteScalarAsync<int>(\"SELECT COUNT(*) FROM pf_UserSession\"));\n\t\t_cacheHelper.SetCacheObject(CacheKeys.CurrentSessionCount, count.Result, 60);\n\t\treturn count.Result;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/SqlObjectFactory.cs",
    "content": "﻿namespace PopForums.Sql;\n\npublic class SqlObjectFactory : ISqlObjectFactory\n{\n\tprivate readonly IConfig _config;\n\n\tpublic SqlObjectFactory(IConfig config)\n\t{\n\t\t_config = config;\n\t}\n\n\tpublic DbConnection GetConnection()\n\t{\n\t\tif (string.IsNullOrWhiteSpace(_config.DatabaseConnectionString))\n\t\t\tthrow new Exception(\"No database connection string found for POP Forums.\");\n\t\tvar connectionString = _config.DatabaseConnectionString;\n\t\treturn new SqlConnection(connectionString);\n\t}\n\n\tpublic DbCommand GetCommand()\n\t{\n\t\treturn new SqlCommand();\n\t}\n\n\tpublic DbCommand GetCommand(string sql)\n\t{\n\t\treturn new SqlCommand(sql);\n\t}\n\n\tpublic DbCommand GetCommand(string sql, DbConnection connection)\n\t{\n\t\treturn new SqlCommand(sql, (SqlConnection)connection);\n\t}\n\n\tpublic DbParameter GetParameter(string parameterName, object value)\n\t{\n\t\treturn new SqlParameter(parameterName, value);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Sql/StreamResponse.cs",
    "content": "namespace PopForums.Sql;\n\npublic class StreamResponse(Stream stream, SqlConnection connection, SqlDataReader reader) : IStreamResponse\n{\n\tpublic Stream Stream => stream;\n\t\n\tpublic void Dispose()\n\t{\n\t\treader.Close();\n\t\tconnection.Close();\n\t\tstream.Close();\n        \n\t\treader.Dispose();\n\t\tconnection.Dispose();\n\t\tstream.Dispose();\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Composers/ForumStateComposerTests.cs",
    "content": "﻿using PopForums.Composers;\n\nnamespace PopForums.Test.Composers;\n\npublic class ForumStateComposerTests\n{\n\tprotected ForumStateComposer GetComposer()\n\t{\n\t\treturn new ForumStateComposer();\n\t}\n\n\tpublic class GetState : ForumStateComposerTests\n\t{\n\t\t[Fact]\n\t\tpublic void MapsCorrectly()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\tvar pagerContext = new PagerContext {PageCount = 1, PageIndex = 2, PageSize = 3};\n\t\t\tvar forum = new Forum {ForumID = 4};\n\n\t\t\tvar result = composer.GetState(forum, pagerContext);\n\n\t\t\tAssert.Equal(forum.ForumID, result.ForumID);\n\t\t\tAssert.Equal(pagerContext.PageIndex, result.PageIndex);\n\t\t\tAssert.Equal(pagerContext.PageSize, result.PageSize);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Composers/PrivateMessageStateComposerTests.cs",
    "content": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing PopForums.Composers;\n\nnamespace PopForums.Test.Composers;\n\npublic class PrivateMessageStateComposerTests\n{\n\tprotected PrivateMessageStateComposer GetComposer()\n\t{\n\t\t_privateMessageService = Substitute.For<IPrivateMessageService>();\n\t\treturn new PrivateMessageStateComposer(_privateMessageService);\n\t}\n\n\tprivate IPrivateMessageService _privateMessageService;\n\n\tpublic class GetState : PrivateMessageStateComposerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task MessagesMappedWithBuffer()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\tvar jsonUsers = JsonSerializer.SerializeToElement(new[] {new {UserID = 2, Name = \"Jeff\"}, new {UserID = 3, Name = \"Diana\"}, new {UserID = 4, Name = \"Simon\"}});\n\t\t\tvar pm = new PrivateMessage { LastViewDate = DateTime.UtcNow, PMID = 123, Users = jsonUsers};\n\t\t\tvar posts = new List<PrivateMessagePost>();\n\t\t\tvar post1 = new PrivateMessagePost{ PMID = pm.PMID, UserID = 2, Name = \"Jeff\", PostTime = new DateTime(2020,1,1), FullText = \"post1\", PMPostID = 1};\n\t\t\tvar post2 = new PrivateMessagePost{ PMID = pm.PMID, UserID = 3, Name = \"Diana\", PostTime = new DateTime(2021,1,1), FullText = \"post2\", PMPostID = 2};\n\t\t\tvar post3 = new PrivateMessagePost{ PMID = pm.PMID, UserID = 4, Name = \"Simon\", PostTime = new DateTime(2022,1,1), FullText = \"post3\", PMPostID = 3};\n\t\t\tposts.Add(post1);\n\t\t\tposts.Add(post2);\n\t\t\t_privateMessageService.GetMostRecentPosts(pm.PMID, pm.LastViewDate).Returns(posts);\n\t\t\t_privateMessageService.GetPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost> { post3 });\n\t\t\t_privateMessageService.GetUsers(pm.PMID).Returns([\n\t\t\t\tnew PrivateMessageUser { PMID = pm.PMID, UserID = 2 },\n\t\t\t\tnew PrivateMessageUser { PMID = pm.PMID, UserID = 3 },\n\t\t\t\tnew PrivateMessageUser { PMID = pm.PMID, UserID = 4 }\n\t\t\t]);\n\n\t\t\tvar state = await composer.GetState(pm);\n\t\t\t\n\t\t\tAssert.Equal(post3.UserID, state.Messages[0].UserID);\n\t\t\tAssert.Equal(post3.Name, state.Messages[0].Name);\n\t\t\tAssert.Equal(post3.PostTime.ToString(\"o\"), state.Messages[0].PostTime);\n\t\t\tAssert.Equal(post3.FullText, state.Messages[0].FullText);\n\t\t\tAssert.Equal(post3.PMPostID, state.Messages[0].PMPostID);\n\t\t\t\n\t\t\tAssert.Equal(post1.UserID, state.Messages[1].UserID);\n\t\t\tAssert.Equal(post1.Name, state.Messages[1].Name);\n\t\t\tAssert.Equal(post1.PostTime.ToString(\"o\"), state.Messages[1].PostTime);\n\t\t\tAssert.Equal(post1.FullText, state.Messages[1].FullText);\n\t\t\tAssert.Equal(post1.PMPostID, state.Messages[1].PMPostID);\n\t\t\t\n\t\t\tAssert.Equal(post2.UserID, state.Messages[2].UserID);\n\t\t\tAssert.Equal(post2.Name, state.Messages[2].Name);\n\t\t\tAssert.Equal(post2.PostTime.ToString(\"o\"), state.Messages[2].PostTime);\n\t\t\tAssert.Equal(post2.FullText, state.Messages[2].FullText);\n\t\t\tAssert.Equal(post2.PMPostID, state.Messages[2].PMPostID);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task NewestPostIDSet()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\tvar jsonUsers = JsonSerializer.SerializeToElement(Array.Empty<object>());\n\t\t\tvar pm = new PrivateMessage { LastViewDate = DateTime.UtcNow, PMID = 123, Users = jsonUsers};\n\t\t\tvar posts = new List<PrivateMessagePost>();\n\t\t\tvar post1 = new PrivateMessagePost{ PMID = pm.PMID };\n\t\t\tvar post2 = new PrivateMessagePost{ PMID = pm.PMID };\n\t\t\tvar post3 = new PrivateMessagePost{ PMID = pm.PMID };\n\t\t\tposts.Add(post1);\n\t\t\tposts.Add(post2);\n\t\t\t_privateMessageService.GetMostRecentPosts(pm.PMID, pm.LastViewDate).Returns(posts);\n\t\t\t_privateMessageService.GetPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost> { post3 });\n\t\t\t_privateMessageService.GetUsers(pm.PMID).Returns(new List<PrivateMessageUser>());\n\n\t\t\tvar state = await composer.GetState(pm);\n\t\t\t\n\t\t\tAssert.Equal(post1.PMPostID, state.NewestPostID);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task PMIDSet()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\tvar jsonUsers = JsonSerializer.SerializeToElement(Array.Empty<object>());\n\t\t\tvar pm = new PrivateMessage { LastViewDate = DateTime.UtcNow, PMID = 123, Users = jsonUsers};\n\t\t\t_privateMessageService.GetMostRecentPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost>());\n\t\t\t_privateMessageService.GetPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost>());\n\t\t\t_privateMessageService.GetUsers(pm.PMID).Returns(new List<PrivateMessageUser>());\n\n\t\t\tvar state = await composer.GetState(pm);\n\t\t\t\n\t\t\tAssert.Equal(pm.PMID, state.PmID);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task PMUsersJsonSet()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\tvar jsonUsers = JsonSerializer.SerializeToElement(Array.Empty<object>());\n\t\t\tvar pm = new PrivateMessage { LastViewDate = DateTime.UtcNow, PMID = 123, Users = jsonUsers};\n\t\t\t_privateMessageService.GetMostRecentPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost>());\n\t\t\t_privateMessageService.GetPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost>());\n\t\t\t_privateMessageService.GetUsers(pm.PMID).Returns(new List<PrivateMessageUser>());\n\n\t\t\tvar state = await composer.GetState(pm);\n\t\t\t\n\t\t\tAssert.Equal(pm.Users, state.Users);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task IsUserNotFoundSetToFalse()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\tvar jsonUsers = JsonSerializer.SerializeToElement(new[] {new {UserID = 2, Name = \"Jeff\"}, new {UserID = 3, Name = \"Diana\"}, new {UserID = 4, Name = \"Simon\"}});\n\t\t\tvar pm = new PrivateMessage { LastViewDate = DateTime.UtcNow, PMID = 123, Users = jsonUsers};\n\t\t\t_privateMessageService.GetMostRecentPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost>());\n\t\t\t_privateMessageService.GetPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost>());\n\t\t\t_privateMessageService.GetUsers(pm.PMID).Returns([\n\t\t\t\tnew PrivateMessageUser { PMID = pm.PMID, UserID = 2 },\n\t\t\t\tnew PrivateMessageUser { PMID = pm.PMID, UserID = 3 },\n\t\t\t\tnew PrivateMessageUser { PMID = pm.PMID, UserID = 4 }\n\t\t\t]);\n\n\t\t\tvar state = await composer.GetState(pm);\n\t\t\t\n\t\t\tAssert.False(state.IsUserNotFound);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task IsUserNotFoundSetToTrue()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\tvar jsonUsers = JsonSerializer.SerializeToElement(new[] {new {UserID = 2, Name = \"Jeff\"}, new {UserID = 3, Name = \"Diana\"}, new {UserID = 4, Name = \"Simon\"}});\n\t\t\tvar pm = new PrivateMessage { LastViewDate = DateTime.UtcNow, PMID = 123, Users = jsonUsers};\n\t\t\t_privateMessageService.GetMostRecentPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost>());\n\t\t\t_privateMessageService.GetPosts(pm.PMID, pm.LastViewDate).Returns(new List<PrivateMessagePost>());\n\t\t\t_privateMessageService.GetUsers(pm.PMID).Returns([\n\t\t\t\tnew PrivateMessageUser { PMID = pm.PMID, UserID = 2 },\n\t\t\t\tnew PrivateMessageUser { PMID = pm.PMID, UserID = 4 }\n\t\t\t]);\n\n\t\t\tvar state = await composer.GetState(pm);\n\t\t\t\n\t\t\tAssert.True(state.IsUserNotFound);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Composers/TopicStateComposerTests.cs",
    "content": "﻿using PopForums.Composers;\n\nnamespace PopForums.Test.Composers;\n\npublic class TopicStateComposerTests\n{\n\tprotected TopicStateComposer GetComposer()\n\t{\n\t\t_userRetrievalShim = Substitute.For<IUserRetrievalShim>();\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_subscribedTopicsService = Substitute.For<ISubscribedTopicsService>();\n\t\t_favoriteTopicService = Substitute.For<IFavoriteTopicService>();\n\t\treturn new TopicStateComposer(_userRetrievalShim, _settingsManager, _subscribedTopicsService, _favoriteTopicService);\n\t}\n\n\tprivate IUserRetrievalShim _userRetrievalShim;\n\tprivate ISettingsManager _settingsManager;\n\tprivate ISubscribedTopicsService _subscribedTopicsService;\n\tprivate IFavoriteTopicService _favoriteTopicService;\n\n\tpublic class GetState : TopicStateComposerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task MapsCorrectlyWithoutUser()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\t_userRetrievalShim.GetUser().Returns((User) null);\n\t\t\tvar topic = new Topic {TopicID = 123, AnswerPostID = 789};\n\n\t\t\tvar result = await composer.GetState(topic, 4, 5, 6);\n\n\t\t\tAssert.Equal(topic.TopicID, result.TopicID);\n\t\t\tAssert.Equal(topic.AnswerPostID, result.AnswerPostID);\n\t\t\tAssert.Equal(4, result.PageIndex);\n\t\t\tAssert.Equal(5, result.PageCount);\n\t\t\tAssert.Equal(6, result.LastVisiblePostID);\n\t\t\tAssert.False(result.IsFavorite);\n\t\t\tAssert.False(result.IsSubscribed);\n\t\t\tAssert.False(result.IsImageEnabled);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task MapsCorrectlyWithUser()\n\t\t{\n\t\t\tvar composer = GetComposer();\n\t\t\tvar user = new User {UserID = 111};\n\t\t\tvar topic = new Topic { TopicID = 123, AnswerPostID = 789 };\n\t\t\t_userRetrievalShim.GetUser().Returns(user);\n\t\t\t_favoriteTopicService.IsTopicFavorite(user.UserID, topic.TopicID).Returns(Task.FromResult(true));\n\t\t\t_subscribedTopicsService.IsTopicSubscribed(user.UserID, topic.TopicID).Returns(Task.FromResult(true));\n\t\t\t_settingsManager.Current.AllowImages.Returns(true);\n\n\t\t\tvar result = await composer.GetState(topic, 4, 5, 6);\n\n\t\t\tAssert.Equal(topic.TopicID, result.TopicID);\n\t\t\tAssert.Equal(topic.AnswerPostID, result.AnswerPostID);\n\t\t\tAssert.Equal(4, result.PageIndex);\n\t\t\tAssert.Equal(5, result.PageCount);\n\t\t\tAssert.Equal(6, result.LastVisiblePostID);\n\t\t\tAssert.True(result.IsFavorite);\n\t\t\tAssert.True(result.IsSubscribed);\n\t\t\tAssert.True(result.IsImageEnabled);\n\t\t\tawait _favoriteTopicService.Received().IsTopicFavorite(user.UserID, topic.TopicID);\n\t\t\tawait _subscribedTopicsService.Received().IsTopicSubscribed(user.UserID, topic.TopicID);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Configuration/SettingsTests.cs",
    "content": "﻿namespace PopForums.Test.Configuration;\n\npublic class SettingsTests\n{\n\t[Fact]\n\tpublic void LoadDefaults()\n\t{\n\t\tvar settingsRepo = Substitute.For<ISettingsRepository>();\n\t\tsettingsRepo.Get().Returns(new Dictionary<string, string>());\n\t\tvar errorLog = Substitute.For<IErrorLog>();\n\t\tvar settingsManager = new SettingsManager(settingsRepo, errorLog);\n\n\t\tvar settings = settingsManager.Current;\n\n\t\tAssert.Equal(settings.TermsOfService, String.Empty);\n\t\tAssert.True(settings.IsNewUserApproved);\n\t\tAssert.Equal(20, settings.TopicsPerPage);\n\t\tAssert.Equal(20, settings.PostsPerPage);\n\t\tAssert.Equal(settings.ForumTitle, String.Empty);\n\t}\n\n\t[Fact]\n\tpublic void LoadFromRepo()\n\t{\n\t\tconst string tos = \"blah blah blah\";\n\t\tconst bool isNewUserApproved = false;\n\t\tconst int topicsPerPage = 72;\n\t\tconst int postsPerPage = 42;\n\t\tconst string title = \"superawesome forum\";\n\t\tvar dictionary = new Dictionary<string, string>\n\t\t{\n\t\t\t{\"TermsOfService\", tos},\n\t\t\t{\"IsNewUserApproved\", isNewUserApproved.ToString()},\n\t\t\t{\"TopicsPerPage\", topicsPerPage.ToString()},\n\t\t\t{\"PostsPerPage\", postsPerPage.ToString()},\n\t\t\t{\"ForumTitle\", title}\n\t\t};\n\t\tvar settingsRepo = Substitute.For<ISettingsRepository>();\n\t\tsettingsRepo.Get().Returns(dictionary);\n\t\tvar errorLog = Substitute.For<IErrorLog>();\n\t\tvar settingsManager = new SettingsManager(settingsRepo, errorLog);\n\n\t\tvar settings = settingsManager.Current;\n\t\t\n\t\tAssert.False(settings.IsNewUserApproved);\n\t\tAssert.Equal(topicsPerPage, settings.TopicsPerPage);\n\t\tAssert.Equal(postsPerPage, settings.PostsPerPage);\n\t\tAssert.Equal(title, settings.ForumTitle);\n\t\tsettingsRepo.Received().Get();\n\t}\n\n\t[Fact]\n\tpublic void LoadFromRepoThenCache()\n\t{\n\t\tvar settingsRepo = Substitute.For<ISettingsRepository>();\n\t\tsettingsRepo.Get().Returns(new Dictionary<string, string>());\n\t\tvar errorLog = Substitute.For<IErrorLog>();\n\t\tvar settingsManager = new SettingsManager(settingsRepo, errorLog);\n\n\t\tvar settings = settingsManager.Current;\n\t\tvar settings2 = settingsManager.Current;\n\t\tvar settings3 = settingsManager.Current;\n\n\t\tAssert.Equal(settings, settings2);\n\t\tAssert.Equal(settings, settings3);\n\t\tsettingsRepo.Received().Get();\n\t}\n\n\t[Fact]\n\tpublic void LoadFromRepoWhenStale()\n\t{\n\t\tvar settingsRepo = Substitute.For<ISettingsRepository>();\n\t\tsettingsRepo.Get().Returns(new Dictionary<string, string>());\n\t\tvar errorLog = Substitute.For<IErrorLog>();\n\t\tvar settingsManager = new SettingsManager(settingsRepo, errorLog);\n\n\t\t_ = settingsManager.Current;\n\t\tsettingsRepo.OnSettingsInvalidated += Raise.Event<Action>();\n\t\t_ = settingsManager.Current;\n\t\tsettingsRepo.Received(2).Get();\n\t}\n\n\t[Fact]\n\tpublic void DoNotLoadFromRepoWhenNotStale()\n\t{\n\t\tvar settingsRepo = Substitute.For<ISettingsRepository>();\n\t\tsettingsRepo.Get().Returns(new Dictionary<string, string>());\n\t\tvar errorLog = Substitute.For<IErrorLog>();\n\t\tvar settingsManager = new SettingsManager(settingsRepo, errorLog);\n\n\t\t_ = settingsManager.Current;\n\t\t_ = settingsManager.Current;\n\t\tsettingsRepo.Received(1).Get();\n\t}\n\n\t[Fact]\n\tpublic void SaveCurrent()\n\t{\n\t\tconst string tos = \"blah blah blah\";\n\t\tconst bool isNewUserApproved = false;\n\t\tconst int topicsPerPage = 72;\n\t\tconst int postsPerPage = 42;\n\t\tconst string title = \"superawesome forum\";\n\t\tconst int minimumSecondsBetweenPosts = 33;\n\t\tconst string smtpServer = \"mail.?.com\";\n\t\tconst int smtpPort = 69;\n\t\tconst string mailerAddress = \"a@b.com\";\n\t\tconst bool useEsmtp = true;\n\t\tconst string smtpUser = \"b@c.com\";\n\t\tconst string smtpPassword = \"jkl\";\n\t\tconst int mailSendingInverval = 500;\n\t\tconst bool useSslSmtp = true;\n\t\tconst int sessionLength = 20;\n\t\tconst string censorWords = \"shit\";\n\t\tconst string censorCharacter = \"x\";\n\t\tconst bool allowImages = true;\n\t\tconst bool logSecurity = false;\n\t\tconst bool logModeration = false;\n\t\tconst bool logErrors = false;\n\t\tconst bool isNewUserImageApproved = true;\n\t\tconst int searchIndexingInterval = 768;\n\t\tconst bool isSearchIndexingEnabled = false;\n\t\tconst bool isMailerEnabled = false;\n\t\tconst int userImageMaxHeight = 999;\n\t\tconst int userImageMaxWidth = 888;\n\t\tconst int userImageMaxkBytes = 77;\n\t\tconst int userAvatarMaxHeight = 665;\n\t\tconst int userAvatarMaxWidth = 554;\n\t\tconst int userAvatarMaxkBytes = 33;\n\t\tconst string mailSignature = \"this is the sig\";\n\t\tconst int awardCalcInterval = 5230;\n\t\tconst int mailerQuantity = 914;\n\t\tconst bool useGoogleLogin = true;\n\t\tconst bool useFacebookLogin = true;\n\t\tconst string facebookAppID = \"oiwoeighw\";\n\t\tconst string facebookAppSecret = \"oiwhwohgcgr\";\n\t\tconst bool useMicrosoftLogin = true;\n\t\tconst string microsoftClientID = \"hhvcwefwege\";\n\t\tconst string microsoftClientSecret = \"oiwhgoigrccaa\";\n\t\tconst int youTubeHeight = 360;\n\t\tconst int youTubeWidth = 640;\n\t\tconst string googleClientId = \"ohigfewgf\";\n\t\tconst string googleClientSecret = \"y0yt0w4gweg\";\n\t\tconst bool useOAuth2Login = true;\n\t\tconst string oAuth2ClientID = \"efew\";\n\t\tconst string oAuth2ClientSecret = \"cons\";\n\t\tconst string oAuth2DisplayName = \"we3t\";\n\t\tconst string oAuth2LoginUrl = \"ef\";\n\t\tconst string oAuth2TokenUrl = \"w\";\n\t\tconst string oAuth2Scope = \"email\";\n\t\tconst bool isClosingAgedTopics = true;\n\t\tconst int closeAgedTopicsDays = 757;\n\t\tconst bool isPrivateForumInstance = true;\n\t\tconst string replyToAddress = \"D@e.com\";\n\t\tconst int postImageMaxHeight = 654;\n\t\tconst int postImageMaxWidth = 980;\n\t\tconst int postImageMaxkBytes = 631;\n\t\tvar dictionary = new Dictionary<string, object>\n\t\t{\n\t\t\t{\"TermsOfService\", tos},\n\t\t\t{\"IsNewUserApproved\", isNewUserApproved},\n\t\t\t{\"TopicsPerPage\", topicsPerPage},\n\t\t\t{\"PostsPerPage\", postsPerPage},\n\t\t\t{\"ForumTitle\", title},\n\t\t\t{\"MinimumSecondsBetweenPosts\", minimumSecondsBetweenPosts},\n\t\t\t{\"SmtpServer\", smtpServer},\n\t\t\t{\"SmtpPort\", smtpPort},\n\t\t\t{\"MailerAddress\", mailerAddress},\n\t\t\t{\"ReplyToAddress\", replyToAddress},\n\t\t\t{\"UseEsmtp\", useEsmtp},\n\t\t\t{\"SmtpUser\", smtpUser},\n\t\t\t{\"SmtpPassword\", smtpPassword},\n\t\t\t{\"MailSendingInverval\", mailSendingInverval},\n\t\t\t{\"UseSslSmtp\", useSslSmtp},\n\t\t\t{\"SessionLength\", sessionLength},\n\t\t\t{\"CensorWords\", censorWords},\n\t\t\t{\"CensorCharacter\", censorCharacter},\n\t\t\t{\"AllowImages\", allowImages},\n\t\t\t{\"LogSecurity\", logSecurity},\n\t\t\t{\"LogModeration\", logModeration},\n\t\t\t{\"LogErrors\", logErrors},\n\t\t\t{\"IsNewUserImageApproved\", isNewUserImageApproved},\n\t\t\t{\"SearchIndexingInterval\", searchIndexingInterval},\n\t\t\t{\"IsSearchIndexingEnabled\", isSearchIndexingEnabled},\n\t\t\t{\"IsMailerEnabled\", isMailerEnabled},\n\t\t\t{\"UserImageMaxHeight\", userImageMaxHeight},\n\t\t\t{\"UserImageMaxWidth\", userImageMaxWidth},\n\t\t\t{\"UserImageMaxkBytes\", userImageMaxkBytes},\n\t\t\t{\"UserAvatarMaxHeight\", userAvatarMaxHeight},\n\t\t\t{\"UserAvatarMaxWidth\", userAvatarMaxWidth},\n\t\t\t{\"UserAvatarMaxkBytes\", userAvatarMaxkBytes},\n\t\t\t{\"MailSignature\", mailSignature},\n\t\t\t{\"ScoringGameCalculatorInterval\", awardCalcInterval},\n\t\t\t{\"MailerQuantity\", mailerQuantity},\n\t\t\t{\"UseGoogleLogin\", useGoogleLogin},\n\t\t\t{\"UseFacebookLogin\", useFacebookLogin},\n\t\t\t{\"FacebookAppID\", facebookAppID},\n\t\t\t{\"FacebookAppSecret\", facebookAppSecret},\n\t\t\t{\"UseMicrosoftLogin\", useMicrosoftLogin},\n\t\t\t{\"MicrosoftClientID\", microsoftClientID},\n\t\t\t{\"MicrosoftClientSecret\", microsoftClientSecret},\n\t\t\t{\"YouTubeHeight\", youTubeHeight},\n\t\t\t{\"YouTubeWidth\", youTubeWidth},\n\t\t\t{\"GoogleClientId\", googleClientId},\n\t\t\t{\"GoogleClientSecret\", googleClientSecret},\n\t\t\t{\"UseOAuth2Login\", useOAuth2Login},\n\t\t\t{\"OAuth2ClientID\", oAuth2ClientID },\n\t\t\t{\"OAuth2ClientSecret\", oAuth2ClientSecret },\n\t\t\t{\"OAuth2LoginUrl\", oAuth2LoginUrl },\n\t\t\t{\"OAuth2TokenUrl\", oAuth2TokenUrl },\n\t\t\t{\"OAuth2DisplayName\", oAuth2DisplayName },\n\t\t\t{\"OAuth2Scope\", oAuth2Scope },\n\t\t\t{\"IsClosingAgedTopics\", isClosingAgedTopics},\n\t\t\t{\"CloseAgedTopicsDays\", closeAgedTopicsDays},\n\t\t\t{\"IsPrivateForumInstance\", isPrivateForumInstance},\n\t\t\t{\"PostImageMaxHeight\", postImageMaxHeight},\n\t\t\t{\"PostImageMaxWidth\", postImageMaxWidth},\n\t\t\t{\"PostImageMaxkBytes\", postImageMaxkBytes}\n\t\t};\n\n\t\tvar settingsRepo = Substitute.For<ISettingsRepository>();\n\t\tsettingsRepo.Get().Returns(new Dictionary<string, string>());\n\t\tvar errorLog = Substitute.For<IErrorLog>();\n\t\tvar settingsManager = new SettingsManager(settingsRepo, errorLog);\n\n\t\tvar settings = settingsManager.Current;\n\t\tsettings.TermsOfService = tos;\n\t\tsettings.IsNewUserApproved = isNewUserApproved;\n\t\tsettings.TopicsPerPage = topicsPerPage;\n\t\tsettings.PostsPerPage = postsPerPage;\n\t\tsettings.ForumTitle = title;\n\t\tsettings.MinimumSecondsBetweenPosts = minimumSecondsBetweenPosts;\n\t\tsettings.SmtpServer = smtpServer;\n\t\tsettings.SmtpPort = smtpPort;\n\t\tsettings.MailerAddress = mailerAddress;\n\t\tsettings.ReplyToAddress = replyToAddress;\n\t\tsettings.UseEsmtp = useEsmtp;\n\t\tsettings.SmtpUser = smtpUser;\n\t\tsettings.SmtpPassword = smtpPassword;\n\t\tsettings.MailSendingInverval = mailSendingInverval;\n\t\tsettings.UseSslSmtp = useSslSmtp;\n\t\tsettings.SessionLength = sessionLength;\n\t\tsettings.CensorWords = censorWords;\n\t\tsettings.CensorCharacter = censorCharacter;\n\t\tsettings.AllowImages = allowImages;\n\t\tsettings.LogSecurity = logSecurity;\n\t\tsettings.LogModeration = logModeration;\n\t\tsettings.LogErrors = logErrors;\n\t\tsettings.IsNewUserImageApproved = isNewUserImageApproved;\n\t\tsettings.SearchIndexingInterval = searchIndexingInterval;\n\t\tsettings.IsSearchIndexingEnabled = isSearchIndexingEnabled;\n\t\tsettings.IsMailerEnabled = isMailerEnabled;\n\t\tsettings.UserImageMaxHeight = userImageMaxHeight;\n\t\tsettings.UserImageMaxWidth = userImageMaxWidth;\n\t\tsettings.UserImageMaxkBytes = userImageMaxkBytes;\n\t\tsettings.UserAvatarMaxHeight = userAvatarMaxHeight;\n\t\tsettings.UserAvatarMaxWidth = userAvatarMaxWidth;\n\t\tsettings.UserAvatarMaxkBytes = userAvatarMaxkBytes;\n\t\tsettings.MailSignature = mailSignature;\n\t\tsettings.ScoringGameCalculatorInterval = awardCalcInterval;\n\t\tsettings.MailerQuantity = mailerQuantity;\n\t\tsettings.UseGoogleLogin = useGoogleLogin;\n\t\tsettings.UseFacebookLogin = useFacebookLogin;\n\t\tsettings.FacebookAppID = facebookAppID;\n\t\tsettings.FacebookAppSecret = facebookAppSecret;\n\t\tsettings.UseMicrosoftLogin = useMicrosoftLogin;\n\t\tsettings.MicrosoftClientID = microsoftClientID;\n\t\tsettings.MicrosoftClientSecret = microsoftClientSecret;\n\t\tsettings.YouTubeHeight = youTubeHeight;\n\t\tsettings.YouTubeWidth = youTubeWidth;\n\t\tsettings.GoogleClientId = googleClientId;\n\t\tsettings.GoogleClientSecret = googleClientSecret;\n\t\tsettings.UseOAuth2Login = useOAuth2Login;\n\t\tsettings.OAuth2ClientID = oAuth2ClientID;\n\t\tsettings.OAuth2ClientSecret = oAuth2ClientSecret;\n\t\tsettings.OAuth2LoginUrl = oAuth2LoginUrl;\n\t\tsettings.OAuth2TokenUrl = oAuth2TokenUrl;\n\t\tsettings.OAuth2DisplayName = oAuth2DisplayName;\n\t\tsettings.OAuth2Scope = oAuth2Scope;\n\t\tsettings.IsClosingAgedTopics = isClosingAgedTopics;\n\t\tsettings.CloseAgedTopicsDays = closeAgedTopicsDays;\n\t\tsettings.IsPrivateForumInstance = isPrivateForumInstance;\n\t\tsettings.PostImageMaxHeight = postImageMaxHeight;\n\t\tsettings.PostImageMaxWidth = postImageMaxWidth;\n\t\tsettings.PostImageMaxkBytes = postImageMaxkBytes;\n\t\tsettingsManager.SaveCurrent();\n\n\t\tsettingsRepo.Received().Save(Arg.Is<Dictionary<string, object>>(x => x.SequenceEqual(dictionary)));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Email/EmailWorkerTests.cs",
    "content": "using NSubstitute.Extensions;\nusing NSubstitute.ReturnsExtensions;\n\nnamespace PopForums.Test.Email;\n\npublic class EmailWorkerTests\n{\n\tprivate ISettingsManager _settingsManager;\n\tprivate ISmtpWrapper _smtpWrapper;\n\tprivate IQueuedEmailMessageRepository _queuedEmailMessageRepository;\n\tprivate IEmailQueueRepository _emailQueueRepository;\n\tprivate IErrorLog _errorLog;\n\n\tprivate IEmailWorker GetWorker()\n\t{\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_smtpWrapper = Substitute.For<ISmtpWrapper>();\n\t\t_queuedEmailMessageRepository = Substitute.For<IQueuedEmailMessageRepository>();\n\t\t_emailQueueRepository = Substitute.For<IEmailQueueRepository>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\treturn new EmailWorker(_settingsManager, _smtpWrapper, _queuedEmailMessageRepository, _emailQueueRepository,\n\t\t\t_errorLog);\n\t}\n\t\n\t[Fact]\n\tpublic void DoNothingWhenNoPayload()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_emailQueueRepository.Dequeue().ReturnsNull();\n\t\t\n\t\tworker.Execute();\n\n\t\t_queuedEmailMessageRepository.DidNotReceiveWithAnyArgs().GetMessage(Arg.Any<int>());\n\t\t_queuedEmailMessageRepository.DidNotReceiveWithAnyArgs().DeleteMessage(Arg.Any<int>());\n\t\t_smtpWrapper.DidNotReceiveWithAnyArgs().Send(Arg.Any<QueuedEmailMessage>());\n\t\t_errorLog.DidNotReceiveWithAnyArgs().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\t\n\t[Fact]\n\tpublic async Task LogExceptionWhenEmailQueuePayloadTypeIsDeleteMassMessage()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_settingsManager.Current.MailerQuantity.Returns(4);\n\t\t_emailQueueRepository.Dequeue().Returns(new EmailQueuePayload { EmailQueuePayloadType = EmailQueuePayloadType.DeleteMassMessage });\n\t\t\n\t\tawait worker.Execute();\n\n\t\t_errorLog.Received(1).Log(Arg.Any<NotImplementedException>(), ErrorSeverity.Error);\n\t}\n\n    [Fact]\n    public async Task ProcessAMessage()\n    {\n        var worker = GetWorker();\n        _settingsManager.Current.MailerQuantity.Returns(4);\n        var payload = new EmailQueuePayload { EmailQueuePayloadType = EmailQueuePayloadType.MassMessage, MessageID = 1, ToEmail = \"test@example.com\", ToName = \"Test User\" };\n        var queuedMessage = new QueuedEmailMessage { MessageID = 1 };\n        _emailQueueRepository.Dequeue().Returns(payload, null, null);\n        _queuedEmailMessageRepository.GetMessage(1).Returns(queuedMessage);\n\n        await worker.Execute();\n\n        Assert.Equal(\"test@example.com\", queuedMessage.ToEmail);\n        Assert.Equal(\"Test User\", queuedMessage.ToName);\n        await _queuedEmailMessageRepository.Received(1).DeleteMessage(1);\n        _smtpWrapper.Received(1).Send(queuedMessage);\n    }\n\n    [Fact]\n    public async Task DoNothingWhenQueuedMessageIsNull()\n    {\n        var worker = GetWorker();\n        _settingsManager.Current.MailerQuantity.Returns(4);\n        var payload = new EmailQueuePayload { EmailQueuePayloadType = EmailQueuePayloadType.MassMessage, MessageID = 1 };\n        _emailQueueRepository.Dequeue().Returns(payload);\n        _queuedEmailMessageRepository.GetMessage(1).ReturnsNull();\n\n        await worker.Execute();\n\n        await _queuedEmailMessageRepository.DidNotReceiveWithAnyArgs().DeleteMessage(Arg.Any<int>());\n        _smtpWrapper.DidNotReceiveWithAnyArgs().Send(Arg.Any<QueuedEmailMessage>());\n    }\n\n    [Fact]\n    public async Task LogExceptionWhenSmtpSendFails()\n    {\n        var worker = GetWorker();\n        _settingsManager.Current.MailerQuantity.Returns(4);\n        var payload = new EmailQueuePayload { EmailQueuePayloadType = EmailQueuePayloadType.DeleteMassMessage, MessageID = 1 };\n        var queuedMessage = new QueuedEmailMessage { MessageID = 1 };\n        _emailQueueRepository.Dequeue().Returns(payload);\n        _queuedEmailMessageRepository.GetMessage(1).Returns(queuedMessage);\n        _smtpWrapper.When(x => x.Send(queuedMessage)).Do(_ => throw new Exception());\n\n        await worker.Execute();\n\n        _errorLog.Received(1).Log(Arg.Any<Exception>(), ErrorSeverity.Error);\n    }\n}"
  },
  {
    "path": "src/PopForums.Test/Email/NewAccountMailerTests.cs",
    "content": "﻿namespace PopForums.Test.Email;\n\npublic class NewAccountMailerTests\n{\n\t[Fact]\n\tpublic void SendCallsSmtpWrapper()\n\t{\n\t\tvar wrapper = Substitute.For<ISmtpWrapper>();\n\t\tvar resultMessage = new EmailMessage();\n\t\twrapper.Send(Arg.Do<EmailMessage>(msg => resultMessage = msg)).Returns(SmtpStatusCode.Ok);\n\t\tconst string mailerAddress = \"a@b.com\";\n\t\tconst string forumTitle = \"superawesome\";\n\t\tvar user = UserTest.GetTestUser();\n\t\tvar settings = new Settings { MailerAddress = mailerAddress, ForumTitle = forumTitle};\n\t\tvar settingsManager = Substitute.For<ISettingsManager>();\n\t\tsettingsManager.Current.Returns(settings);\n\t\tvar mailer = new NewAccountMailer(settingsManager, wrapper);\n\n\t\tvar result = mailer.Send(user, \"http://blah/\");\n\n\t\tAssert.Equal(SmtpStatusCode.Ok, result);\n\t\tAssert.Equal(resultMessage.ToName, user.Name);\n\t\tAssert.Equal(resultMessage.ToEmail, user.Email);\n\t\tAssert.Equal(forumTitle, resultMessage.FromName);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Extensions/StringTests.cs",
    "content": "﻿namespace PopForums.Test.Extensions;\n\npublic class StringTests\n{\n\t[Fact]\n\tpublic void GetSHA256HashString()\n\t{\n\t\tvar output = \"fred\".GetSHA256Hash();\n\t\tAssert.Equal(\"0M/C5TGbgs3HGjOHPoJsk9fuETY/iskcT6Oiz80ihuU=\", output);\n\t}\n\n\t[Fact]\n\tpublic void GetMD5HashString()\n\t{\n\t\tvar output = \"fred\".GetMD5Hash();\n\t\tAssert.Equal(\"VwqQv7+MfqtdxdTiaDLVsQ==\", output);\n\t}\n\n\t[Fact]\n\tpublic void IsEmailTest()\n\t{\n\t\tAssert.True(\"a@b.com\".IsEmailAddress());\n\t\tAssert.True(\"scottgu@microsoft.com\".IsEmailAddress());\n\t\tAssert.True(\"obama@whitehouse.gov\".IsEmailAddress());\n\t\tAssert.True(\"a_b@c.net\".IsEmailAddress());\n\t\tAssert.True(\"a.b@site.co.uk\".IsEmailAddress());\n\t\tAssert.True(\"ora@mixedmedia.studio\".IsEmailAddress());\n\t}\n\n\t[Fact]\n\tpublic void IsNoteEmailTest()\n\t{\n\t\tAssert.False(\"a@c\".IsEmailAddress());\n\t\tAssert.False(\"abc@examplecom\".IsEmailAddress());\n\t\tAssert.False(\"ora.mixedmedia.studio\".IsEmailAddress());\n\t\tAssert.False(\"a a@c.com\".IsEmailAddress());\n\t\tAssert.False(\"aa@c a.com\".IsEmailAddress());\n\t\tAssert.False(\"aa@coishd!iwe.com\".IsEmailAddress());\n\t}\n\n\t[Fact]\n\tpublic void UrlNameTest()\n\t{\n\t\tAssert.Equal(\"abc-def-ghi\", \"abc def ghi\".ToUrlName());\n\t\tAssert.Equal(\"abcdef-ghi\", \"abcdef-ghi\".ToUrlName());\n\t\tAssert.Equal(\"abcdef-ghi\", \"abc.def-ghi\".ToUrlName());\n\t\tAssert.Equal(\"abcdefghi\", \"abc#def/ghi\".ToUrlName());\n\t\tAssert.Equal(\"abc----defghi\", \"abc    def*ghi\".ToUrlName());\n\t}\n\n\t[Fact]\n\tpublic void UrlNameUniqueTest()\n\t{\n\t\tvar list = new List<string> { \"forum-title\", \"forum-title-but-not\", \"forum-title-2\" };\n\t\tconst string title = \"forum-title\";\n\t\tvar result = title.ToUniqueUrlName(list);\n\t\tAssert.Equal(\"forum-title-3\", result);\n\t}\n\n\t[Fact]\n\tpublic void UrlNameUniqueTestForPlantedDupe()\n\t{\n\t\tvar list = new List<string> { \"forum-title\", \"forum-title-2\" };\n\t\tconst string title = \"forum-title\";\n\t\tvar result = title.ToUniqueUrlName(list);\n\t\tAssert.Equal(\"forum-title-3\", result);\n\t}\n\n\t[Fact]\n\tpublic void UrlNameUniqueTestForDoubleDigits()\n\t{\n\t\tvar list = new List<string> { \"forum-title\", \"forum-title-1\", \"forum-title-2\", \"forum-title-3\", \"forum-title-4\", \"forum-title-5\", \"forum-title-6\", \"forum-title-7\", \"forum-title-8\", \"forum-title-9\", \"forum-title-10\", \"forum-title-11\" };\n\t\tconst string title = \"forum-title\";\n\t\tvar result = title.ToUniqueUrlName(list);\n\t\tAssert.Equal(\"forum-title-12\", result);\n\t}\n\n\t[Fact]\n\tpublic void TrimmerTest()\n\t{\n\t\tvar result = \"123456789012345678901234567890\".Trimmer(22);\n\t\tAssert.Equal(\"123456789...1234567890\", result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/ExternalLogin/ExternalUserAssociationManagerTests.cs",
    "content": "﻿namespace PopForums.Test.ExternalLogin;\n\npublic class ExternalUserAssociationManagerTests\n{\n\tprivate ExternalUserAssociationManager GetManager()\n\t{\n\t\t_externalUserAssociationRepo = Substitute.For<IExternalUserAssociationRepository>();\n\t\t_userRepo = Substitute.For<IUserRepository>();\n\t\t_securityLogService = Substitute.For<ISecurityLogService>();\n\t\treturn new ExternalUserAssociationManager(_externalUserAssociationRepo, _userRepo, _securityLogService);\n\t}\n\n\tprivate IExternalUserAssociationRepository _externalUserAssociationRepo;\n\tprivate IUserRepository _userRepo;\n\tprivate ISecurityLogService _securityLogService;\n\n\t[Fact]\n\tpublic async Task ExternalUserAssociationCheckThrowsWithNullArg()\n\t{\n\t\tvar manager = GetManager();\n\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(async () => await manager.ExternalUserAssociationCheck(null, \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task ExternalUserAssociationCheckResultFalseWithNullsIfNoMatchingAssociation()\n\t{\n\t\tvar manager = GetManager();\n\t\t_externalUserAssociationRepo.Get(Arg.Any<string>(), Arg.Any<string>()).Returns((ExternalUserAssociation) null);\n\n\t\tvar result = await manager.ExternalUserAssociationCheck(new ExternalLoginInfo(\"\", \"\", \"\"), \"\");\n\n\t\tAssert.False(result.Successful);\n\t\tAssert.Null(result.ExternalUserAssociation);\n\t\tAssert.Null(result.User);\n\t}\n\n\t[Fact]\n\tpublic async Task ExternalUserAssociationCheckResultFalseWithNullsIfNoMatchingUser()\n\t{\n\t\tvar manager = GetManager();\n\t\t_externalUserAssociationRepo.Get(Arg.Any<string>(), Arg.Any<string>()).Returns(Task.FromResult(new ExternalUserAssociation()));\n\t\t_userRepo.GetUser(Arg.Any<int>()).Returns((User) null);\n\n\t\tvar result = await manager.ExternalUserAssociationCheck(new ExternalLoginInfo(\"\", \"\", \"\"), \"\");\n\n\t\tAssert.False(result.Successful);\n\t\tAssert.Null(result.ExternalUserAssociation);\n\t\tAssert.Null(result.User);\n\t}\n\n\t[Fact]\n\tpublic async Task ExternalUserAssociationCheckResultTrueWithHydratedResultIfMatchingAssociationAndUser()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar association = new ExternalUserAssociation { Issuer = \"Google\", UserID = 123, ProviderKey = \"abc\"};\n\t\tvar user = new User {UserID = association.UserID};\n\t\t_externalUserAssociationRepo.Get(association.Issuer, association.ProviderKey).Returns(Task.FromResult(association));\n\t\t_userRepo.GetUser(association.UserID).Returns(Task.FromResult(user));\n\t\tvar authResult = new ExternalLoginInfo(\"Google\", \"abc\", \"\");\n\n\t\tvar result = await manager.ExternalUserAssociationCheck(authResult, \"\");\n\n\t\tAssert.True(result.Successful);\n\t\tAssert.Same(user, result.User);\n\t\tAssert.Same(association, result.ExternalUserAssociation);\n\t}\n\n\t[Fact]\n\tpublic async Task ExternalUserAssociationCheckResultTrueCallsSecurityLog()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar association = new ExternalUserAssociation { Issuer = \"Google\", UserID = 123, ProviderKey = \"abc\" };\n\t\tvar user = new User {UserID = association.UserID};\n\t\t_externalUserAssociationRepo.Get(association.Issuer, association.ProviderKey).Returns(Task.FromResult(association));\n\t\t_userRepo.GetUser(association.UserID).Returns(Task.FromResult(user));\n\t\tconst string ip = \"1.1.1.1\";\n\t\tvar authResult = new ExternalLoginInfo(\"Google\", \"abc\", \"\");\n\n\t\tawait manager.ExternalUserAssociationCheck(authResult, ip);\n\n\t\tawait _securityLogService.Received().CreateLogEntry(user, user, ip, Arg.Any<string>(), SecurityLogType.ExternalAssociationCheckSuccessful);\n\t}\n\n\t[Fact]\n\tpublic async Task ExternalUserAssociationCheckResultFalseNoMatchCallsSecurityLog()\n\t{\n\t\tvar manager = GetManager();\n\t\t_externalUserAssociationRepo.Get(Arg.Any<string>(), Arg.Any<string>()).Returns((ExternalUserAssociation)null);\n\t\tconst string ip = \"1.1.1.1\";\n\t\tvar authResult = new ExternalLoginInfo(\"Google\", \"abc\", \"\");\n\n\t\tawait manager.ExternalUserAssociationCheck(authResult, ip);\n\n\t\tawait _securityLogService.Received().CreateLogEntry((int?)null, null, ip, Arg.Any<string>(), SecurityLogType.ExternalAssociationCheckFailed);\n\t}\n\n\t[Fact]\n\tpublic async Task ExternalUserAssociationCheckResultFalseNoUserCallsSecurityLog()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar association = new ExternalUserAssociation { Issuer = \"Google\", UserID = 123, ProviderKey = \"abc\" };\n\t\t_externalUserAssociationRepo.Get(association.Issuer, association.ProviderKey).Returns(Task.FromResult(association));\n\t\t_userRepo.GetUser(association.UserID).Returns((User)null);\n\t\tconst string ip = \"1.1.1.1\";\n\t\tvar authResult = new ExternalLoginInfo(\"Google\", \"abc\", \"\");\n\n\t\tawait manager.ExternalUserAssociationCheck(authResult, ip);\n\n\t\tawait _securityLogService.Received().CreateLogEntry((int?)null, null, ip, Arg.Any<string>(), SecurityLogType.ExternalAssociationCheckFailed);\n\t}\n\n\t[Fact]\n\tpublic async Task AssociateThrowsWithNullUser()\n\t{\n\t\tvar manager = GetManager();\n\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(async () => await manager.Associate(null, default, String.Empty));\n\t}\n\n\t[Fact]\n\tpublic async Task AssociateNeverCallsRepoWithNullExternalAuthResult()\n\t{\n\t\tvar manager = GetManager();\n\n\t\tawait manager.Associate(new User(), null, String.Empty);\n\n\t\tawait _externalUserAssociationRepo.DidNotReceive().Save(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());\n\t}\n\n\t[Fact]\n\tpublic async Task AssociateThrowsWithNoProviderKey()\n\t{\n\t\tvar manager = GetManager();\n\n\t\tawait Assert.ThrowsAsync<NullReferenceException>(async () => await manager.Associate(new User(), new ExternalLoginInfo(\"efwef\", \"\", \"\"), string.Empty));\n\t}\n\n\t[Fact]\n\tpublic async Task AssociateMapsObjectsToRepoCall()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar user = new User {UserID = 123};\n\t\tvar externalAuthResult = new ExternalLoginInfo(\"wegggw\", \"wfweg\", \"wewg\");\n\n\t\tawait manager.Associate(user, externalAuthResult, String.Empty);\n\n\t\tawait _externalUserAssociationRepo.Received().Save(user.UserID, externalAuthResult.LoginProvider, externalAuthResult.ProviderKey, externalAuthResult.ProviderDisplayName);\n\t}\n\n\t[Fact]\n\tpublic async Task AssociateSuccessCallsSecurityLog()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar externalAuthResult = new ExternalLoginInfo(\"wegggw\", \"wfweg\", \"wewg\");\n\t\tconst string ip = \"1.1.1.1\";\n\n\t\tawait manager.Associate(user, externalAuthResult, ip);\n\n\t\tawait _securityLogService.Received().CreateLogEntry(user, user, ip, Arg.Any<string>(), SecurityLogType.ExternalAssociationSet);\n\t}\n\n\t[Fact]\n\tpublic async Task GetExternalUserAssociationsCallsRepoByUserID()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar user = new User { UserID = 123 };\n\t\tawait _externalUserAssociationRepo.GetByUser(user.UserID);\n\n\t\tawait manager.GetExternalUserAssociations(user);\n\n\t\tawait _externalUserAssociationRepo.Received().GetByUser(user.UserID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetExternalUserAssociationsReturnsCollectionFromRepo()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar collection = new List<ExternalUserAssociation>();\n\t\t_externalUserAssociationRepo.GetByUser(user.UserID).Returns(Task.FromResult(collection));\n\n\t\tvar result = await manager.GetExternalUserAssociations(user);\n\n\t\tAssert.Same(collection, result);\n\t}\n\n\t[Fact]\n\tpublic async Task RemoveAssociationNeverCallsRepoIfNoAssociationIsFound()\n\t{\n\t\tvar manager = GetManager();\n\t\t_externalUserAssociationRepo.Get(Arg.Any<int>()).Returns((ExternalUserAssociation) null);\n\n\t\tawait manager.RemoveAssociation(new User(), 4556, String.Empty);\n\n\t\tawait _externalUserAssociationRepo.DidNotReceive().Delete(Arg.Any<int>());\n\t}\n\n\t[Fact]\n\tpublic async Task RemoveAssociationLogsTheRemoval()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar association = new ExternalUserAssociation {ExternalUserAssociationID = 123, Issuer = \"Google\", Name = \"Jeffy\", ProviderKey = \"oihfoihfef\", UserID = 456};\n\t\tvar user = new User {UserID = association.UserID};\n\t\tconst string ip = \"1.1.1.1\";\n\t\t_externalUserAssociationRepo.Get(association.ExternalUserAssociationID).Returns(Task.FromResult(association));\n\n\t\tawait manager.RemoveAssociation(user, association.ExternalUserAssociationID, ip);\n\n\t\tawait _securityLogService.Received().CreateLogEntry(user, user, ip, Arg.Any<string>(), SecurityLogType.ExternalAssociationRemoved);\n\t}\n\n\t[Fact]\n\tpublic async Task RemoveAssociationThrowsIfUserIDsDontMatch()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar association = new ExternalUserAssociation { ExternalUserAssociationID = 123, UserID = 456 };\n\t\tvar user = new User { UserID = 789 };\n\t\t_externalUserAssociationRepo.Get(association.ExternalUserAssociationID).Returns(Task.FromResult(association));\n\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await manager.RemoveAssociation(user, association.ExternalUserAssociationID, string.Empty));\n\t}\n\n\t[Fact]\n\tpublic async Task RemoveAssociationCallsRepoOnSuccessfulMatch()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar association = new ExternalUserAssociation { ExternalUserAssociationID = 123, UserID = 456 };\n\t\tvar user = new User { UserID = association.UserID };\n\t\t_externalUserAssociationRepo.Get(association.ExternalUserAssociationID).Returns(Task.FromResult(association));\n\n\t\tawait manager.RemoveAssociation(user, association.ExternalUserAssociationID, String.Empty);\n\n\t\tawait _externalUserAssociationRepo.Received().Delete(association.ExternalUserAssociationID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetExternalUserAssociationsThrowsIfAssociationDoesntMatchUser()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar association = new ExternalUserAssociation { ExternalUserAssociationID = 456, UserID = 789};\n\t\tvar user = new User();\n\t\t_externalUserAssociationRepo.Get(association.ExternalUserAssociationID).Returns(Task.FromResult(association));\n\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await manager.RemoveAssociation(user, association.ExternalUserAssociationID, string.Empty));\n\t}\n\n\t[Fact]\n\tpublic async Task GetExternalUserAssociationsCallsRepoWithMatchingUserIDs()\n\t{\n\t\tvar manager = GetManager();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar association = new ExternalUserAssociation { ExternalUserAssociationID = 456, UserID = user.UserID };\n\t\t_externalUserAssociationRepo.Get(association.ExternalUserAssociationID).Returns(Task.FromResult(association));\n\n\t\tawait manager.RemoveAssociation(user, association.ExternalUserAssociationID, String.Empty);\n\n\t\tawait _externalUserAssociationRepo.Received().Delete(association.ExternalUserAssociationID);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Global.cs",
    "content": "﻿global using System;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\nglobal using System.Reflection;\nglobal using System.Security;\nglobal using System.Text.RegularExpressions;\nglobal using System.Threading.Tasks;\n\nglobal using Microsoft.AspNetCore.Mvc;\n\nglobal using PopForums.Configuration;\nglobal using PopForums.Email;\nglobal using PopForums.Extensions;\nglobal using PopForums.ExternalLogin;\nglobal using PopForums.Feeds;\nglobal using PopForums.Messaging;\nglobal using PopForums.Models;\nglobal using PopForums.Repositories;\nglobal using PopForums.ScoringGame;\nglobal using PopForums.Services;\n\nglobal using PopForums.Mvc.Areas.Forums.Controllers;\nglobal using PopForums.Mvc.Areas.Forums.Services;\n\nglobal using PopForums.Test.Models;\n\nglobal using NSubstitute;\nglobal using Xunit;"
  },
  {
    "path": "src/PopForums.Test/Messaging/NotificationAdapterTests.cs",
    "content": "﻿using PopForums.Messaging.Models;\n\nnamespace PopForums.Test.Messaging;\n\npublic class NotificationAdapterTests\n{\n\tprotected NotificationAdapter GetAdapter()\n\t{\n\t\t_notificationManager = Substitute.For<INotificationManager>();\n\t\treturn new NotificationAdapter(_notificationManager);\n\t}\n\n\tprivate INotificationManager _notificationManager;\n\n\tpublic class Reply : NotificationAdapterTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ManagerCalledWithCorrectValues()\n\t\t{\n\t\t\tvar adapter = GetAdapter();\n\t\t\tvar name = \"Jeff\";\n\t\t\tvar title = \"The Topic\";\n\t\t\tvar topicID = 123;\n\t\t\tvar userID = 456;\n\t\t\tvar tenantID = \"cb\";\n\t\t\tReplyData replyData = null;\n\t\t\tawait _notificationManager.ProcessNotification(userID, NotificationType.NewReply, topicID, Arg.Do<ReplyData>(x => replyData = x), tenantID);\n\n\t\t\tawait adapter.Reply(name, title, topicID, userID, tenantID);\n\n\t\t\tAssert.Equal(name, replyData.PostName);\n\t\t\tAssert.Equal(topicID, replyData.TopicID);\n\t\t\tAssert.Equal(title, replyData.Title);\n\t\t\tawait _notificationManager.Received().ProcessNotification(userID, NotificationType.NewReply, topicID, Arg.Any<ReplyData>(), tenantID);\n\t\t}\n\t}\n\n\tpublic class Vote : NotificationAdapterTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ManagerCalledWithCorrectValues()\n\t\t{\n\t\t\tvar adapter = GetAdapter();\n\t\t\tvar name = \"Jeff\";\n\t\t\tvar title = \"The Topic\";\n\t\t\tvar postID = 123;\n\t\t\tvar userID = 456;\n\t\t\tVoteData voteData = null;\n\t\t\tawait _notificationManager.ProcessNotification(userID, NotificationType.VoteUp, postID, Arg.Do<VoteData>(x => voteData = x));\n\n\t\t\tawait adapter.Vote(name, title, postID, userID);\n\n\t\t\tAssert.Equal(name, voteData.VoterName);\n\t\t\tAssert.Equal(title, voteData.Title);\n\t\t\tAssert.Equal(postID, voteData.PostID);\n\t\t\tawait _notificationManager.Received().ProcessNotification(userID, NotificationType.VoteUp, postID, Arg.Any<VoteData>());\n\t\t}\n\t}\n\n\tpublic class QuestionAnswer : NotificationAdapterTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ManagerCalledWithCorrectValues()\n\t\t{\n\t\t\tvar adapter = GetAdapter();\n\t\t\tvar askerName = \"Jeff\";\n\t\t\tvar title = \"The Topic\";\n\t\t\tvar postID = 123;\n\t\t\tvar userID = 456;\n\t\t\tQuestionData questionData = null;\n\t\t\tawait _notificationManager.ProcessNotification(userID, NotificationType.QuestionAnswered, postID, Arg.Do<QuestionData>(x => questionData = x));\n\n\t\t\tawait adapter.QuestionAnswer(askerName, title, postID, userID);\n\n\t\t\tAssert.Equal(askerName, questionData.AskerName);\n\t\t\tAssert.Equal(title, questionData.Title);\n\t\t\tAssert.Equal(postID, questionData.PostID);\n\t\t\tawait _notificationManager.Received().ProcessNotification(userID, NotificationType.QuestionAnswered, postID, Arg.Any<QuestionData>());\n\t\t}\n\t}\n\n\tpublic class Award : NotificationAdapterTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ManagerCalledWithCorrectValues()\n\t\t{\n\t\t\tvar adapter = GetAdapter();\n\t\t\tvar title = \"The Award\";\n\t\t\tvar userID = 456;\n\t\t\tAwardData awardData = null;\n\t\t\tawait _notificationManager.ProcessNotification(userID, NotificationType.Award, Arg.Any<long>(), Arg.Do<AwardData>(x => awardData = x), null);\n\n\t\t\tawait adapter.Award(title, userID);\n\t\t\t\n\t\t\tawait _notificationManager.Received().ProcessNotification(userID, NotificationType.Award, Arg.Any<long>(), Arg.Any<AwardData>(), null);\n\t\t\tAssert.Equal(title, awardData.Title);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Messaging/NotificationManagerTests.cs",
    "content": "﻿using System.Text.Json;\n\nnamespace PopForums.Test.Messaging;\n\npublic class NotificationManagerTests\n{\n\tprotected NotificationManager GetManager()\n\t{\n\t\t_notificationRepository = Substitute.For<INotificationRepository>();\n\t\t_broker = Substitute.For<IBroker>();\n\t\treturn new NotificationManager(_notificationRepository, _broker);\n\t}\n\n\tprivate INotificationRepository _notificationRepository;\n\tprivate IBroker _broker;\n\n\tpublic class ProcessNotification : NotificationManagerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task FieldsMapToUpdate()\n\t\t{\n\t\t\tvar manager = GetManager();\n\t\t\tvar userID = 1;\n\t\t\tvar contextID = 2;\n\t\t\tvar notificationType = NotificationType.NewReply;\n\t\t\tvar data = new {a = 123, b = \"xyz\"};\n\t\t\tvar unreadCount = 42;\n\t\t\tNotification result = null;\n\t\t\t_notificationRepository.UpdateNotification(Arg.Do<Notification>(x => result = x)).Returns(Task.FromResult(1));\n\t\t\t_notificationRepository.GetUnreadNotificationCount(userID).Returns(Task.FromResult(unreadCount));\n\n\t\t\tawait manager.ProcessNotification(userID, notificationType, contextID, data);\n\n\t\t\tAssert.Equal(userID, result.UserID);\n\t\t\tAssert.False(result.IsRead);\n\t\t\tAssert.Equal(notificationType, result.NotificationType);\n\t\t\tAssert.Equal(contextID, result.ContextID);\n\t\t\tvar serializedData = JsonSerializer.SerializeToElement(data);\n\t\t\tAssert.Equal(serializedData.ToString(), result.Data.ToString());\n\t\t\tAssert.Equal(unreadCount, result.UnreadCount);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CreateNotCalledOnSuccessfulUpdate()\n\t\t{\n\t\t\tvar manager = GetManager();\n\t\t\t_notificationRepository.UpdateNotification(Arg.Any<Notification>()).Returns(Task.FromResult(1));\n\n\t\t\tawait manager.ProcessNotification(1, NotificationType.NewReply, 2, new {});\n\t\t\t\n\t\t\tawait _notificationRepository.DidNotReceive().CreateNotification(Arg.Any<Notification>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task FieldsMapToCreate()\n\t\t{\n\t\t\tvar manager = GetManager();\n\t\t\tvar userID = 1;\n\t\t\tvar contextID = 2;\n\t\t\tvar notificationType = NotificationType.NewReply;\n\t\t\tvar data = new { a = 123, b = \"xyz\" };\n\t\t\tNotification result = null;\n\t\t\t_notificationRepository.UpdateNotification(Arg.Any<Notification>()).Returns(Task.FromResult(0));\n\t\t\tawait _notificationRepository.CreateNotification(Arg.Do<Notification>(x => result = x));\n\n\t\t\tawait manager.ProcessNotification(userID, notificationType, contextID, data);\n\n\t\t\tAssert.Equal(userID, result.UserID);\n\t\t\tAssert.False(result.IsRead);\n\t\t\tAssert.Equal(notificationType, result.NotificationType);\n\t\t\tAssert.Equal(contextID, result.ContextID);\n\t\t\tvar serializedData = JsonSerializer.SerializeToElement(data);\n\t\t\tAssert.Equal(serializedData.ToString(), result.Data.ToString());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task FieldsMapToNotifyUser()\n\t\t{\n\t\t\tvar manager = GetManager();\n\t\t\tvar userID = 1;\n\t\t\tvar contextID = 2;\n\t\t\tvar notificationType = NotificationType.NewReply;\n\t\t\tvar data = new { a = 123, b = \"xyz\" };\n\t\t\tNotification result = null;\n\t\t\t_notificationRepository.UpdateNotification(Arg.Any<Notification>()).Returns(Task.FromResult(1));\n\t\t\t_broker.NotifyUser(Arg.Do<Notification>(x => result = x));\n\n\t\t\tawait manager.ProcessNotification(userID, notificationType, contextID, data);\n\n\t\t\tAssert.Equal(userID, result.UserID);\n\t\t\tAssert.False(result.IsRead);\n\t\t\tAssert.Equal(notificationType, result.NotificationType);\n\t\t\tAssert.Equal(contextID, result.ContextID);\n\t\t\tvar serializedData = JsonSerializer.SerializeToElement(data);\n\t\t\tAssert.Equal(serializedData.ToString(), result.Data.ToString());\n\t\t}\n\t}\n\n\tpublic class GetUnreadNotificationCount : NotificationManagerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task Over100Returns100()\n\t\t{\n\t\t\tvar manager = GetManager();\n\t\t\tconst int userID = 123;\n\t\t\t_notificationRepository.GetUnreadNotificationCount(userID).Returns(Task.FromResult(101));\n\n\t\t\tvar result = await manager.GetUnreadNotificationCount(userID);\n\n\t\t\tAssert.Equal(100, result);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task Under100ReturnsRepoValue()\n\t\t{\n\t\t\tvar manager = GetManager();\n\t\t\tconst int userID = 123;\n\t\t\t_notificationRepository.GetUnreadNotificationCount(userID).Returns(Task.FromResult(99));\n\n\t\t\tvar result = await manager.GetUnreadNotificationCount(userID);\n\n\t\t\tAssert.Equal(99, result);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task The100Returns100()\n\t\t{\n\t\t\tvar manager = GetManager();\n\t\t\tconst int userID = 123;\n\t\t\t_notificationRepository.GetUnreadNotificationCount(userID).Returns(Task.FromResult(100));\n\n\t\t\tvar result = await manager.GetUnreadNotificationCount(userID);\n\n\t\t\tAssert.Equal(100, result);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Models/ForumHomeContainerTests.cs",
    "content": "﻿namespace PopForums.Test.Models;\n\npublic class ForumHomeContainerTests\n{\n\t[Fact]\n\tpublic void UncategorizedForumsShowUpOnProperty()\n\t{\n\t\tvar c1 = new Category { CategoryID = 1 };\n\t\tvar c2 = new Category { CategoryID = 2 };\n\t\tvar f1 = new Forum {ForumID = 1, CategoryID = null};\n\t\tvar f2 = new Forum { ForumID = 2, CategoryID = 1};\n\t\tvar f3 = new Forum { ForumID = 3, CategoryID = 2};\n\t\tvar f4 = new Forum { ForumID = 4, CategoryID = 0};\n\t\tvar cats = new List<Category> {c1, c2};\n\t\tvar forums = new List<Forum> {f1, f2, f3, f4};\n\t\tvar container = new CategorizedForumContainer(cats, forums);\n\t\tAssert.Contains(f1, container.UncategorizedForums);\n\t\tAssert.DoesNotContain(f2, container.UncategorizedForums);\n\t\tAssert.DoesNotContain(f3, container.UncategorizedForums);\n\t\tAssert.Contains(f4, container.UncategorizedForums);\n\t}\n\n\t[Fact]\n\tpublic void UncategorizedInCorrectOrder()\n\t{\n\t\tvar f1 = new Forum { ForumID = 1, SortOrder = 5 };\n\t\tvar f2 = new Forum { ForumID = 2, SortOrder = 1 };\n\t\tvar f3 = new Forum { ForumID = 3, SortOrder = 3 };\n\t\tvar forums = new List<Forum> { f1, f2, f3 };\n\t\tvar container = new CategorizedForumContainer(new List<Category>(), forums);\n\t\tAssert.True(container.UncategorizedForums[0] == f2);\n\t\tAssert.True(container.UncategorizedForums[1] == f3);\n\t\tAssert.True(container.UncategorizedForums[2] == f1);\n\t}\n\n\t[Fact]\n\tpublic void CategoriesInCorrectOrder()\n\t{\n\t\tvar c1 = new Category { CategoryID = 1, SortOrder = 5 };\n\t\tvar c2 = new Category { CategoryID = 2, SortOrder = 1 };\n\t\tvar c3 = new Category { CategoryID = 3, SortOrder = 3 };\n\t\tvar f1 = new Forum { ForumID = 1, CategoryID = 1 };\n\t\tvar f2 = new Forum { ForumID = 2, CategoryID = 2 };\n\t\tvar f3 = new Forum { ForumID = 3, CategoryID = 3 };\n\t\tvar cats = new List<Category> {c1, c2, c3};\n\t\tvar forums = new List<Forum> {f1, f2, f3};\n\t\tvar container = new CategorizedForumContainer(cats, forums);\n\t\tAssert.True(container.CategoryDictionary.ToArray()[0].Key == c2);\n\t\tAssert.True(container.CategoryDictionary.ToArray()[1].Key == c3);\n\t\tAssert.True(container.CategoryDictionary.ToArray()[2].Key == c1);\n\t}\n\n\t[Fact]\n\tpublic void AllCollectionsPersist()\n\t{\n\t\tvar c1 = new Category { CategoryID = 1 };\n\t\tvar c2 = new Category { CategoryID = 2 };\n\t\tvar f1 = new Forum { ForumID = 1, CategoryID = null };\n\t\tvar f2 = new Forum { ForumID = 2, CategoryID = 1 };\n\t\tvar f3 = new Forum { ForumID = 3, CategoryID = 2 };\n\t\tvar cats = new List<Category> { c1, c2 };\n\t\tvar forums = new List<Forum> { f1, f2, f3 };\n\t\tvar container = new CategorizedForumContainer(cats, forums);\n\t\tAssert.Equal(cats, container.AllCategories);\n\t\tAssert.Equal(forums, container.AllForums);\n\t}\n\n\t[Fact]\n\tpublic void ForumsAppearInCategories()\n\t{\n\t\tvar c1 = new Category { CategoryID = 1, Title = \"Cat1\" };\n\t\tvar c2 = new Category { CategoryID = 2, Title = \"Cat2\" };\n\t\tvar f1 = new Forum { ForumID = 1, CategoryID = null };\n\t\tvar f2 = new Forum { ForumID = 2, CategoryID = 1 };\n\t\tvar f3 = new Forum { ForumID = 3, CategoryID = 2 };\n\t\tvar cats = new List<Category> { c1, c2 };\n\t\tvar forums = new List<Forum> { f1, f2, f3 };\n\t\tvar container = new CategorizedForumContainer(cats, forums);\n\t\tAssert.Contains(f2, container.CategoryDictionary[c1]);\n\t\tAssert.Contains(f3, container.CategoryDictionary[c2]);\n\t\tAssert.DoesNotContain(f1, container.CategoryDictionary[c1]);\n\t\tAssert.DoesNotContain(f3, container.CategoryDictionary[c1]);\n\t}\n\t\t\n\t[Fact]\n\tpublic void CategoryWithNoForumsDoesNotAppear()\n\t{\n\t\tvar c1 = new Category { CategoryID = 1, Title = \"Cat1\" };\n\t\tvar c2 = new Category { CategoryID = 2, Title = \"Cat2\" };\n\t\tvar c3 = new Category { CategoryID = 3, Title = \"Cat3\" };\n\t\tvar f1 = new Forum { ForumID = 1, CategoryID = null };\n\t\tvar f2 = new Forum { ForumID = 2, CategoryID = 1 };\n\t\tvar f3 = new Forum { ForumID = 3, CategoryID = 2 };\n\t\tvar cats = new List<Category> { c1, c2, c3 };\n\t\tvar forums = new List<Forum> { f1, f2, f3 };\n\t\tvar container = new CategorizedForumContainer(cats, forums);\n\t\tAssert.False(container.CategoryDictionary.ContainsKey(c3));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Models/UserEditSecurityTests.cs",
    "content": "﻿namespace PopForums.Test.Models;\n\npublic class UserEditSecurityTests\n{\n\t[Fact]\n\tpublic void PasswordsMatch()\n\t{\n\t\tvar edit = new UserEditSecurity();\n\t\tedit.NewPassword = \"blah\";\n\t\tedit.NewPasswordRetype = \"blah\";\n\t\tAssert.True(edit.NewPasswordsMatch());\n\t}\n\n\t[Fact]\n\tpublic void PasswordsNoMatch()\n\t{\n\t\tvar edit = new UserEditSecurity();\n\t\tedit.NewPassword = \"blasjspvjsh\";\n\t\tedit.NewPasswordRetype = \"blah\";\n\t\tAssert.False(edit.NewPasswordsMatch());\n\t}\n\n\t[Fact]\n\tpublic void EmailMatch()\n\t{\n\t\tvar edit = new UserEditSecurity();\n\t\tedit.NewEmail = \"blah\";\n\t\tedit.NewEmailRetype = \"blah\";\n\t\tAssert.True(edit.NewEmailsMatch());\n\t}\n\n\t[Fact]\n\tpublic void EmailNoMatch()\n\t{\n\t\tvar edit = new UserEditSecurity();\n\t\tedit.NewEmail = \"blah\";\n\t\tedit.NewEmailRetype = \"bloidsvosah\";\n\t\tAssert.False(edit.NewEmailsMatch());\n\t}\n\n\t[Fact]\n\tpublic void IsNewUserApprovedMapped()\n\t{\n\t\tvar edit = new UserEditSecurity(new User { UserID = 1 }, true);\n\t\tAssert.True(edit.IsNewUserApproved);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Models/UserTest.cs",
    "content": "﻿namespace PopForums.Test.Models;\n\npublic class UserTest\n{\n\t[Fact]\n\tpublic void IsRoleWiredToRoles()\n\t{\n\t\tvar user = GetTestUser();\n\t\tuser.Roles = new List<string> {\"blah\", \"three\", PermanentRoles.Admin};\n\t\tAssert.True(user.IsInRole(PermanentRoles.Admin));\n\t}\n\n\tpublic static User GetTestUser()\n\t{\n\t\tconst int userID = 123;\n\t\tconst string name = \"Jeff\";\n\t\tconst string email = \"a@b.com\";\n\t\tvar createDate = DateTime.UtcNow;\n\t\tconst bool approved = true;\n\t\tvar authKey = Guid.NewGuid();\n\t\treturn new User { UserID = userID, Name = name, Email = email, CreationDate = createDate, IsApproved = approved, AuthorizationKey = authKey, Roles = new List<string>() };\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Mvc/Authorization/PopForumsPrivateForumsFilterTests.cs",
    "content": "using Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc.Abstractions;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.AspNetCore.Routing;\nusing PopForums.Mvc.Areas.Forums.Authorization;\n\nnamespace PopForums.Test.Mvc.Authorization;\n\npublic class PopForumsPrivateForumsFilterTests\n{\n\tprivate PopForumsPrivateForumsFilter GetFilter()\n\t{\n\t\t_userRetrievalShim = Substitute.For<IUserRetrievalShim>();\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_config = Substitute.For<IConfig>();\n\t\treturn new PopForumsPrivateForumsFilter(_userRetrievalShim, _settingsManager, _config);\n\t}\n\n\tprivate ActionExecutingContext GetContext()\n\t{\n\t\treturn new ActionExecutingContext(new ActionContext{HttpContext = new DefaultHttpContext(), RouteData = new RouteData(), ActionDescriptor = new ActionDescriptor()}, new List<IFilterMetadata>(), new Dictionary<string, object>(), null);\n\t}\n\n\tprivate IUserRetrievalShim _userRetrievalShim;\n\tprivate ISettingsManager _settingsManager;\n\tprivate IConfig _config;\n\n\tpublic class OnActionExecuting : PopForumsPrivateForumsFilterTests\n\t{\n\t\t[Fact]\n\t\tpublic void DoesNothingIfSettingIsFalseAndOAuthOnlyIsFalse()\n\t\t{\n\t\t\tvar filter = GetFilter();\n\t\t\t_config.IsOAuthOnly.Returns(false);\n\t\t\t_settingsManager.Current.IsPrivateForumInstance.Returns(false);\n\t\t\tvar context = GetContext();\n\t\t\t\n\t\t\tfilter.OnActionExecuting(context);\n\t\t\t\n\t\t\t_userRetrievalShim.DidNotReceive().GetUser();\n\t\t\tAssert.Null(context.Result);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void DoesNothingIfSettingIsTrueAndUserPresent()\n\t\t{\n\t\t\tvar filter = GetFilter();\n\t\t\t_config.IsOAuthOnly.Returns(false);\n\t\t\t_settingsManager.Current.IsPrivateForumInstance.Returns(true);\n\t\t\tvar context = GetContext();\n\t\t\tvar user = new User();\n\t\t\t_userRetrievalShim.GetUser().Returns(user);\n\t\t\t\n\t\t\tfilter.OnActionExecuting(context);\n\t\t\t\n\t\t\t_userRetrievalShim.Received().GetUser();\n\t\t\tAssert.Null(context.Result);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void DoesNothingIfOAuthOnlyIsTrueAndUserPresent()\n\t\t{\n\t\t\tvar filter = GetFilter();\n\t\t\t_config.IsOAuthOnly.Returns(true);\n\t\t\t_settingsManager.Current.IsPrivateForumInstance.Returns(false);\n\t\t\tvar context = GetContext();\n\t\t\tvar user = new User();\n\t\t\t_userRetrievalShim.GetUser().Returns(user);\n\t\t\t\n\t\t\tfilter.OnActionExecuting(context);\n\t\t\t\n\t\t\t_userRetrievalShim.Received().GetUser();\n\t\t\tAssert.Null(context.Result);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void RedirectIfSettingIsTrueAndNoUserPresent()\n\t\t{\n\t\t\tvar filter = GetFilter();\n\t\t\t_config.IsOAuthOnly.Returns(false);\n\t\t\t_settingsManager.Current.IsPrivateForumInstance.Returns(true);\n\t\t\tvar context = GetContext();\n\t\t\t\n\t\t\tfilter.OnActionExecuting(context);\n\t\t\t\n\t\t\t_userRetrievalShim.Received().GetUser();\n\t\t\tAssert.IsType<RedirectToActionResult>(context.Result);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void RedirectIfOAuthOnlyIsTrueAndNoUserPresent()\n\t\t{\n\t\t\tvar filter = GetFilter();\n\t\t\t_config.IsOAuthOnly.Returns(true);\n\t\t\t_settingsManager.Current.IsPrivateForumInstance.Returns(false);\n\t\t\tvar context = GetContext();\n\t\t\t\n\t\t\tfilter.OnActionExecuting(context);\n\t\t\t\n\t\t\t_userRetrievalShim.Received().GetUser();\n\t\t\tAssert.IsType<RedirectToActionResult>(context.Result);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Mvc/Controllers/AccountControllerTests.cs",
    "content": "﻿using System.Net;\nusing Microsoft.AspNetCore.Http;\nusing PopForums.Mvc.Areas.Forums.Models;\nusing PopIdentity;\n\nnamespace PopForums.Test.Mvc.Controllers;\n\npublic class AccountControllerTests\n{\n\tprivate IUserService _userService;\n\tprivate IProfileService _profileService;\n\tprivate ISettingsManager _settingsManager;\n\tprivate INewAccountMailer _newAccountMailer;\n\tprivate IPostService _postService;\n\tprivate ITopicService _topicService;\n\tprivate IForumService _forumService;\n\tprivate ILastReadService _lastReadService;\n\tprivate IImageService _imageService;\n\tprivate IFeedService _feedService;\n\tprivate IUserAwardService _userAwardService;\n\tprivate IExternalUserAssociationManager _externalUserAssocManager;\n\tprivate IUserRetrievalShim _userRetrievalShim;\n\tprivate IExternalLoginRoutingService _externalLoginRoutingService;\n\tprivate IExternalLoginTempService _externalLoginTempService;\n\tprivate IConfig _config;\n\tprivate IReCaptchaService _recaptchaService;\n\tprivate IOAuthOnlyService _oAuthOnlyService;\n\n\tprivate AccountController GetController()\n\t{\n\t\t_userService = Substitute.For<IUserService>();\n\t\t_profileService = Substitute.For<IProfileService>();\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_newAccountMailer = Substitute.For<INewAccountMailer>();\n\t\t_postService = Substitute.For<IPostService>();\n\t\t_topicService = Substitute.For<ITopicService>();\n\t\t_forumService = Substitute.For<IForumService>();\n\t\t_lastReadService = Substitute.For<ILastReadService>();\n\t\t_imageService = Substitute.For<IImageService>();\n\t\t_feedService = Substitute.For<IFeedService>();\n\t\t_userAwardService = Substitute.For<IUserAwardService>();\n\t\t_externalUserAssocManager = Substitute.For<IExternalUserAssociationManager>();\n\t\t_userRetrievalShim = Substitute.For<IUserRetrievalShim>();\n\t\t_externalLoginRoutingService = Substitute.For<IExternalLoginRoutingService>();\n\t\t_externalLoginTempService = Substitute.For<IExternalLoginTempService>();\n\t\t_config = Substitute.For<IConfig>();\n\t\t_recaptchaService = Substitute.For<IReCaptchaService>();\n\t\t_oAuthOnlyService = Substitute.For<IOAuthOnlyService>();\n\t\tvar controller = new AccountController(_userService, _profileService, _newAccountMailer, _settingsManager, _postService, _topicService, _forumService, _lastReadService, _imageService, _feedService, _userAwardService, _externalUserAssocManager, _userRetrievalShim, _externalLoginRoutingService, _externalLoginTempService, _config, _recaptchaService, _oAuthOnlyService);\n\t\tcontroller.ControllerContext = new ControllerContext\n\t\t{\n\t\t\tHttpContext = new DefaultHttpContext()\n\t\t};\n\t\tcontroller.HttpContext.Connection.RemoteIpAddress = IPAddress.Loopback;\n\t\treturn controller;\n\t}\n\t\n\tpublic class Create : AccountControllerTests\n\t{\n\t\t[Fact]\n\t\tpublic void PopulatesDefaultValues()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\t_settingsManager.Current.TermsOfService.Returns(\"tos\");\n\n\t\t\tvar result = controller.Create();\n\t\t\t\n\t\t\tAssert.Equal(\"tos\", (result as ViewResult)?.ViewData[AccountController.TosKey]);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic void PopulatesValuesFromExternalLogin()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\t_settingsManager.Current.TermsOfService.Returns(\"tos\");\n\t\t\tvar externalLoginState = new ExternalLoginState\n\t\t\t{\n\t\t\t\tResultData = new ResultData\n\t\t\t\t{\n\t\t\t\t\tEmail = \"a@b.com\",\n\t\t\t\t\tName = \"Diana\"\n\t\t\t\t}\n\t\t\t};\n\t\t\t_externalLoginTempService.Read().Returns(externalLoginState);\n\n\t\t\tvar result = controller.Create();\n\n\t\t\tvar signupData = (SignupData)(result as ViewResult)?.Model;\n\t\t\tAssert.Equal(\"tos\", (result as ViewResult)?.ViewData[AccountController.TosKey]);\n\t\t\tAssert.Equal(externalLoginState.ResultData.Email, signupData.Email);\n\t\t\tAssert.Equal(externalLoginState.ResultData.Name, signupData.Name);\n\t\t}\n\t}\n\n\tpublic class Verify : AccountControllerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ReturnVerifyFailViewWhenNonGuidCode()\n\t\t{\n\t\t\tvar controller = GetController();\n\n\t\t\tvar result = await controller.Verify(\"notaguid\");\n\t\t\t\n\t\t\tAssert.Equal(\"VerifyFail\", result.ViewName);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ReturnDefaultViewWhenNoCode()\n\t\t{\n\t\t\tvar controller = GetController();\n\n\t\t\tvar result = await controller.Verify(\"\");\n\t\t\t\n\t\t\tAssert.Null(result.ViewName);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ReturnVerifyFailViewWhenGuidMatchesNoUser()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\t_userService.VerifyAuthorizationCode(Arg.Any<Guid>(), Arg.Any<string>()).Returns((User)null);\n\n\t\t\tvar result = await controller.Verify(\"920A89D6-CE1B-4EBE-B758-50DB514B0ABF\");\n\t\t\t\n\t\t\tAssert.Equal(\"VerifyFail\", result.ViewName);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task SuccessReturnViewWithMessage()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar user = new User();\n\t\t\t_userService.VerifyAuthorizationCode(Guid.Parse(\"920A89D6-CE1B-4EBE-B758-50DB514B0ABF\"), Arg.Any<string>()).Returns(Task.FromResult(user));\n\n\t\t\tvar result = await controller.Verify(\"920A89D6-CE1B-4EBE-B758-50DB514B0ABF\");\n\t\t\t\n\t\t\tAssert.Null(result.ViewName);\n\t\t\tAssert.Equal(Resources.AccountVerified, result.ViewData[\"Result\"]);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task SuccessLoginUser()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar user = new User();\n\t\t\t_userService.VerifyAuthorizationCode(Guid.Parse(\"920A89D6-CE1B-4EBE-B758-50DB514B0ABF\"), Arg.Any<string>()).Returns(Task.FromResult(user));\n\n\t\t\tvar result = await controller.Verify(\"920A89D6-CE1B-4EBE-B758-50DB514B0ABF\");\n\t\t\t\n\t\t\tawait _userService.Received().Login(user, Arg.Any<string>());\n\t\t}\n\t}\n\t\n\tpublic class VerifyCode : AccountControllerTests\n\t{\n\t\t[Fact]\n\t\tpublic void RedirectsToVerify()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar code = \"ED89EDB4-FBDD-494E-9ACF-2FE2AD69D21D\";\n\n\t\t\tvar result = controller.VerifyCode(code);\n\n\t\t\tAssert.IsType<RedirectToActionResult>(result);\n\t\t\tAssert.Equal(\"Verify\", result.ActionName);\n\t\t\tAssert.Equal(code, result.RouteValues?[\"id\"]);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Mvc/Controllers/AdminApiControllerTests.cs",
    "content": "﻿namespace PopForums.Test.Mvc.Controllers;\n\npublic class AdminApiControllerTests\n{\n\tprivate ISettingsManager _settingsManager;\n\tprivate ICategoryService _categoryService;\n\tprivate IForumService _forumService;\n\tprivate IUserService _userService;\n\tprivate ISearchService _searchService;\n\tprivate IProfileService _profileService;\n\tprivate IUserRetrievalShim _userRetrievalShim;\n\tprivate IImageService _imageService;\n\tprivate IBanService _banService;\n\tprivate IMailingListService _mailingListService;\n\tprivate IEventDefinitionService _eventDefService;\n\tprivate IAwardDefinitionService _awardDefService;\n\tprivate IEventPublisher _eventPublisher;\n\tprivate IIPHistoryService _ipHistoryService;\n\tprivate ISecurityLogService _securityLogService;\n\tprivate IModerationLogService _moderationLogService;\n\tprivate IErrorLog _errorLog;\n\tprivate IServiceHeartbeatService _serviceHeartbeatService;\n\n\tprivate AdminApiController GetController()\n\t{\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_categoryService = Substitute.For<ICategoryService>();\n\t\t_forumService = Substitute.For<IForumService>();\n\t\t_userService = Substitute.For<IUserService>();\n\t\t_searchService = Substitute.For<ISearchService>();\n\t\t_profileService = Substitute.For<IProfileService>();\n\t\t_userRetrievalShim = Substitute.For<IUserRetrievalShim>();\n\t\t_imageService = Substitute.For<IImageService>();\n\t\t_banService = Substitute.For<IBanService>();\n\t\t_mailingListService = Substitute.For<IMailingListService>();\n\t\t_eventDefService = Substitute.For<IEventDefinitionService>();\n\t\t_awardDefService = Substitute.For<IAwardDefinitionService>();\n\t\t_eventPublisher = Substitute.For<IEventPublisher>();\n\t\t_ipHistoryService = Substitute.For<IIPHistoryService>();\n\t\t_securityLogService = Substitute.For<ISecurityLogService>();\n\t\t_moderationLogService = Substitute.For<IModerationLogService>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\t_serviceHeartbeatService = Substitute.For<IServiceHeartbeatService>();\n\t\treturn new AdminApiController(_settingsManager, _categoryService, _forumService, _userService, _searchService, _profileService, _userRetrievalShim, _imageService, _banService, _mailingListService, _eventDefService, _awardDefService, _eventPublisher, _ipHistoryService, _securityLogService, _moderationLogService, _errorLog, _serviceHeartbeatService);\n\t}\n\n\tpublic class SaveForum : AdminApiControllerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task CallsCreateIfForumIDIsZero()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar forum = new Forum {ForumID = 0, CategoryID = 1, Title = \"tt\", Description = \"dd\", IsVisible = true, IsArchived = true, IsQAForum = true, ForumAdapterName = \"ff\"};\n\n\t\t\tawait controller.SaveForum(forum);\n\n\t\t\tawait _forumService.Received().Create(forum.CategoryID, forum.Title, forum.Description, forum.IsVisible, forum.IsArchived, -1, forum.ForumAdapterName, forum.IsQAForum);\n\t\t\tawait _forumService.DidNotReceive().Update(Arg.Any<Forum>(), Arg.Any<int?>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<string>(), Arg.Any<bool>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsUpdateIfForumIDIsNotZero()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar forum = new Forum { ForumID = 123, CategoryID = 1, Title = \"tt\", Description = \"dd\", IsVisible = true, IsArchived = true, IsQAForum = true, ForumAdapterName = \"ff\" };\n\t\t\tvar retrievedForum = new Forum();\n\t\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(retrievedForum));\n\n\t\t\tawait controller.SaveForum(forum);\n\n\t\t\tawait _forumService.Received().Update(retrievedForum, forum.CategoryID, forum.Title, forum.Description, forum.IsVisible, forum.IsArchived, forum.ForumAdapterName, forum.IsQAForum);\n\t\t\tawait _forumService.DidNotReceive().Create(Arg.Any<int?>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<bool>());\n\t\t}\n\t\t[Fact]\n\t\tpublic async Task ReturnsNotFoundIfForumIsNotReal()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\t_forumService.Get(Arg.Any<int>()).Returns((Forum)null);\n\n\t\t\tvar result = await controller.SaveForum(new Forum{ForumID = 123});\n\n\t\t\tawait _forumService.DidNotReceive().Update(Arg.Any<Forum>(), Arg.Any<int?>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<string>(), Arg.Any<bool>());\n\t\t\tawait _forumService.DidNotReceive().Create(Arg.Any<int?>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<bool>());\n\t\t\tAssert.IsType<NotFoundResult>(result.Result);\n\t\t}\n\t}\n\n\tpublic class GetForumPermissions : AdminApiControllerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ContainerIsComposed()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar forum = new Forum{ForumID = 123};\n\t\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\tvar all = new List<string> {\"a\", \"b\"};\n\t\t\t_userService.GetAllRoles().Returns(Task.FromResult(all));\n\t\t\tvar allView = new List<string> {\"c\", \"d\"};\n\t\t\t_forumService.GetForumViewRoles(forum).Returns(Task.FromResult(allView));\n\t\t\tvar allPost = new List<string> {\"e\", \"f\"};\n\t\t\t_forumService.GetForumPostRoles(forum).Returns(Task.FromResult(allPost));\n\n\t\t\tvar container = await controller.GetForumPermissions(forum.ForumID);\n\n\t\t\tAssert.Equal(forum.ForumID, container.Value.ForumID);\n\t\t\tAssert.Same(all, container.Value.AllRoles);\n\t\t\tAssert.Same(allView, container.Value.ViewRoles);\n\t\t\tAssert.Same(allPost, container.Value.PostRoles);\n\t\t}\n\t}\n\n\tpublic class EditUserSearch : AdminApiControllerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task NameSearchCallsNameSearch()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar text = \"abc\";\n\t\t\tvar list = new List<User>();\n\t\t\t_userService.SearchByName(text).Returns(Task.FromResult(list));\n\n\t\t\tvar result = await controller.EditUserSearch(new UserSearch {SearchText = text, SearchType = UserSearch.UserSearchType.Name});\n\n\t\t\tawait _userService.Received().SearchByName(text);\n\t\t\tAssert.Same(list, result.Value);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task EmailSearchCallsEmailSearch()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar text = \"abc\";\n\t\t\tvar list = new List<User>();\n\t\t\t_userService.SearchByEmail(text).Returns(Task.FromResult(list));\n\n\t\t\tvar result = await controller.EditUserSearch(new UserSearch { SearchText = text, SearchType = UserSearch.UserSearchType.Email });\n\n\t\t\tawait _userService.Received().SearchByEmail(text);\n\t\t\tAssert.Same(list, result.Value);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task RoleSearchCallsRoleSearch()\n\t\t{\n\t\t\tvar controller = GetController();\n\t\t\tvar text = \"abc\";\n\t\t\tvar list = new List<User>();\n\t\t\t_userService.SearchByRole(text).Returns(Task.FromResult(list));\n\n\t\t\tvar result = await controller.EditUserSearch(new UserSearch { SearchText = text, SearchType = UserSearch.UserSearchType.Role });\n\n\t\t\tawait _userService.Received().SearchByRole(text);\n\t\t\tAssert.Same(list, result.Value);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Mvc/Services/OAuthOnlyServiceTests.cs",
    "content": "using System.Security.Claims;\nusing PopIdentity;\nusing PopIdentity.Providers.OAuth2;\n\nnamespace PopForums.Test.Mvc.Services;\n\npublic class OAuthOnlyServiceTests\n{\n\tprivate IConfig _config;\n\tprivate IOAuth2LoginUrlGenerator _oAuth2LoginUrlGen;\n\tprivate IStateHashingService _stateHashingService;\n\tprivate IOAuth2JwtCallbackProcessor _oAuth2JwtCallbackProcessor;\n\tprivate IExternalUserAssociationManager _externalUserAssociationManager;\n\tprivate IUserService _userService;\n\tprivate IClaimsToRoleMapper _claimsToRoleMapper;\n\tprivate IUserNameReconciler _userNameReconciler;\n\tprivate IUserEmailReconciler _userEmailReconciler;\n\tprivate ISecurityLogService _securityLogService;\n\t\n\tprivate OAuthOnlyService GetService()\n\t{\n\t\t_config = Substitute.For<IConfig>();\n\t\t_oAuth2LoginUrlGen = Substitute.For<IOAuth2LoginUrlGenerator>();\n\t\t_stateHashingService = Substitute.For<IStateHashingService>();\n\t\t_oAuth2JwtCallbackProcessor = Substitute.For<IOAuth2JwtCallbackProcessor>();\n\t\t_externalUserAssociationManager = Substitute.For<IExternalUserAssociationManager>();\n\t\t_userService = Substitute.For<IUserService>();\n\t\t_claimsToRoleMapper = Substitute.For<IClaimsToRoleMapper>();\n\t\t_userNameReconciler = Substitute.For<IUserNameReconciler>();\n\t\t_userEmailReconciler = Substitute.For<IUserEmailReconciler>();\n\t\t_securityLogService = Substitute.For<ISecurityLogService>();\n\t\treturn new OAuthOnlyService(_config, _oAuth2LoginUrlGen, _stateHashingService, _oAuth2JwtCallbackProcessor, _externalUserAssociationManager, _userService, _claimsToRoleMapper, _userNameReconciler, _userEmailReconciler, _securityLogService);\n\t}\n\n\tpublic class GetLoginUrl : OAuthOnlyServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic void ConfigParameterAndHashValuesCalledToLoginGen()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_config.OAuthLoginBaseUrl.Returns(\"baseUrl\");\n\t\t\t_config.OAuthClientID.Returns(\"clientID\");\n\t\t\tvar redirectUrl = \"the redirect url\";\n\t\t\tvar code = \"hash\";\n\t\t\t_stateHashingService.SetCookieAndReturnHash().Returns(code);\n\t\t\t_config.OAuthScopes.Returns(\"openid email profile\");\n\n\t\t\tservice.GetLoginUrl(redirectUrl);\n\t\t\t\n\t\t\t_oAuth2LoginUrlGen.Received().GetUrl(_config.OAuthLoginBaseUrl, _config.OAuthClientID, redirectUrl, code, _config.OAuthScopes);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void GeneratedUrlReturned()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar url = \"the return url\";\n\t\t\t_oAuth2LoginUrlGen.GetUrl(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>()).Returns(url);\n\n\t\t\tvar result = service.GetLoginUrl(\"whatever\");\n\t\t\t\n\t\t\tAssert.Equal(url, result);\n\t\t}\n\t}\n\n\tpublic class ProcessOAuthLogin : OAuthOnlyServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task FailedCallbackReturnsFail()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar callbackResult = new CallbackResult { IsSuccessful = false };\n\t\t\t_oAuth2JwtCallbackProcessor.VerifyCallback(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(callbackResult));\n\n\t\t\tvar result = await service.ProcessOAuthLogin(\"url\", \"ip\");\n\t\t\t\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallbackValuesMappedToExternalCheck()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar callbackResult = new CallbackResult { IsSuccessful = true, Claims = new List<Claim>(), ResultData = new ResultData{Email = \"e\", ID = \"i\", Name = \"n\"}};\n\t\t\t_oAuth2JwtCallbackProcessor.VerifyCallback(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(callbackResult));\n\t\t\tvar externalUserMatch = new ExternalUserAssociationMatchResult { User = new User(), Successful = true };\n\t\t\tExternalLoginInfo calledExternalInfo = null;\n\t\t\t_externalUserAssociationManager\n\t\t\t\t.ExternalUserAssociationCheck(Arg.Do<ExternalLoginInfo>(x => calledExternalInfo = x), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(externalUserMatch));\n\t\t\t_config.OAuthRefreshExpirationMinutes.Returns(60);\n\n\t\t\tvar result = await service.ProcessOAuthLogin(\"url\", \"ip\");\n\t\t\t\n\t\t\tAssert.Equal(callbackResult.ResultData.ID, calledExternalInfo.ProviderKey);\n\t\t\tAssert.Equal(callbackResult.ResultData.Name, calledExternalInfo.ProviderDisplayName);\n\t\t\tAssert.Equal(ProviderType.OAuthOnly.ToString(), calledExternalInfo.LoginProvider);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ExistingUserMappedToClaims()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar claims = new Claim[] { new(\"name\", \"value\") };\n\t\t\tvar callbackResult = new CallbackResult { IsSuccessful = true, Claims = claims, ResultData = new ResultData{Email = \"e\", ID = \"i\", Name = \"n\"}};\n\t\t\t_oAuth2JwtCallbackProcessor.VerifyCallback(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(callbackResult));\n\t\t\tvar externalUserMatch = new ExternalUserAssociationMatchResult { User = new User(), Successful = true };\n\t\t\t_externalUserAssociationManager\n\t\t\t\t.ExternalUserAssociationCheck(Arg.Any<ExternalLoginInfo>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(externalUserMatch));\n\t\t\t_config.OAuthRefreshExpirationMinutes.Returns(60);\n\n\t\t\tvar result = await service.ProcessOAuthLogin(\"url\", \"ip\");\n\t\t\t\n\t\t\tawait _claimsToRoleMapper.Received().MapRoles(externalUserMatch.User, claims);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ExistingUserNameUnchangedNotChanged()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar user = new User { Name = \"Simon\" };\n\t\t\tvar callbackResult = new CallbackResult { IsSuccessful = true, Claims = new Claim[]{}, ResultData = new ResultData{Email = \"e\", ID = \"i\", Name = user.Name}};\n\t\t\t_oAuth2JwtCallbackProcessor.VerifyCallback(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(callbackResult));\n\t\t\tvar externalUserMatch = new ExternalUserAssociationMatchResult { User = user, Successful = true };\n\t\t\t_externalUserAssociationManager\n\t\t\t\t.ExternalUserAssociationCheck(Arg.Any<ExternalLoginInfo>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(externalUserMatch));\n\t\t\t_userNameReconciler.GetUniqueNameForUser(Arg.Any<string>()).Returns(Task.FromResult(user.Name));\n\n\t\t\tvar result = await service.ProcessOAuthLogin(\"url\", \"ip\");\n\t\t\t\n\t\t\tawait _userService.DidNotReceive().ChangeName(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<User>(), Arg.Any<string>());\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ExistingUserNameChangedIsChanged()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar user = new User { Name = \"Simon\" };\n\t\t\tvar callbackResult = new CallbackResult { IsSuccessful = true, Claims = new Claim[]{}, ResultData = new ResultData{Email = \"e\", ID = \"i\", Name = \"Jeff\"}};\n\t\t\t_oAuth2JwtCallbackProcessor.VerifyCallback(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(callbackResult));\n\t\t\tvar externalUserMatch = new ExternalUserAssociationMatchResult { User = user, Successful = true };\n\t\t\t_externalUserAssociationManager\n\t\t\t\t.ExternalUserAssociationCheck(Arg.Any<ExternalLoginInfo>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(externalUserMatch));\n\t\t\t_userNameReconciler.GetUniqueNameForUser(Arg.Any<string>()).Returns(Task.FromResult(\"unique\"));\n\n\t\t\tvar result = await service.ProcessOAuthLogin(\"url\", \"ip\");\n\t\t\t\n\t\t\tawait _userService.Received().ChangeName(user, \"unique\", Arg.Any<User>(), Arg.Any<string>());\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task NewUserMappedToClaims()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar claims = new Claim[] { new(\"name\", \"value\") };\n\t\t\tvar callbackResult = new CallbackResult { IsSuccessful = true, Claims = claims, ResultData = new ResultData{Email = \"e\", ID = \"i\", Name = \"n\"}};\n\t\t\t_oAuth2JwtCallbackProcessor.VerifyCallback(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(callbackResult));\n\t\t\tvar externalUserMatch = new ExternalUserAssociationMatchResult { Successful = false };\n\t\t\t_externalUserAssociationManager\n\t\t\t\t.ExternalUserAssociationCheck(Arg.Any<ExternalLoginInfo>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(externalUserMatch));\n\t\t\tvar user = new User();\n\t\t\t_userService.CreateUserWithProfile(Arg.Any<SignupData>(), Arg.Any<string>())\n\t\t\t\t.Returns(Task.FromResult(user));\n\t\t\t_config.OAuthRefreshExpirationMinutes.Returns(60);\n\n\t\t\tvar result = await service.ProcessOAuthLogin(\"url\", \"ip\");\n\t\t\t\n\t\t\tawait _claimsToRoleMapper.Received().MapRoles(user, claims);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task UnmatchedUserIsCreatedAndAssociated()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar redirUrl = \"redir\";\n\t\t\tvar ip = \"127.0.0.1\";\n\t\t\tvar callbackResult = new CallbackResult\n\t\t\t{\n\t\t\t\tResultData = new ResultData\n\t\t\t\t{\n\t\t\t\t\tEmail = \"a@b.com\",\n\t\t\t\t\tID = \"id\",\n\t\t\t\t\tName = \"Diana\"\n\t\t\t\t},\n\t\t\t\tIsSuccessful = true\n\t\t\t};\n\t\t\t_config.OAuthTokenUrl.Returns(\"t\");\n\t\t\t_config.OAuthClientID.Returns(\"c\");\n\t\t\t_config.OAuthClientSecret.Returns(\"s\");\n\t\t\t_oAuth2JwtCallbackProcessor.VerifyCallback(redirUrl, \"t\", \"c\", \"s\").Returns(Task.FromResult(callbackResult));\n\t\t\tExternalLoginInfo externalLoginInfo = null;\n\t\t\t_externalUserAssociationManager\n\t\t\t\t.ExternalUserAssociationCheck(Arg.Do<ExternalLoginInfo>(x => externalLoginInfo = x), ip)\n\t\t\t\t.Returns(Task.FromResult(new ExternalUserAssociationMatchResult { Successful = false }));\n\t\t\tvar user = new User();\n\t\t\tSignupData signupData = null;\n\t\t\t_userService.CreateUserWithProfile(Arg.Do<SignupData>(x => signupData = x), ip).Returns(Task.FromResult(user));\n\t\t\t_userNameReconciler.GetUniqueNameForUser(\"Diana\").Returns(Task.FromResult(\"UniqueD\"));\n\t\t\t_userEmailReconciler.GetUniqueEmail(\"a@b.com\", \"id\").Returns(Task.FromResult(\"uid\"));\n\n\t\t\tawait service.ProcessOAuthLogin(redirUrl, ip);\n\t\t\t\n\t\t\tAssert.Equal(callbackResult.ResultData.ID, externalLoginInfo.ProviderKey);\n\t\t\tAssert.Equal(\"UniqueD\", signupData.Name);\n\t\t\tAssert.Equal(\"uid\", signupData.Email);\n\t\t\tawait _userService.Received().CreateUserWithProfile(signupData, ip);\n\t\t\tawait _externalUserAssociationManager.Received().Associate(user, externalLoginInfo, ip);\n\t\t}\n\t}\n\n\tpublic class AttemptTokenRefresh : OAuthOnlyServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task FailedCallbackMakesNoUpdates()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_oAuth2JwtCallbackProcessor.GetRefreshToken(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>()).Returns(Task.FromResult(new CallbackResult { IsSuccessful = false }));\n\n\t\t\tawait service.AttemptTokenRefresh(default);\n\t\t\t\n\t\t\tawait _userService.DidNotReceive().UpdateTokenExpiration(Arg.Any<User>(), Arg.Any<DateTime>());\n\t\t\tawait _userService.DidNotReceive().UpdateRefreshToken(Arg.Any<User>(), Arg.Any<string>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task GoodTokenUpdatesStuff()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar user = new User();\n\t\t\tvar token = \"token\";\n\t\t\tvar newToken = \"newtoken\";\n\t\t\t_userService.GetRefreshToken(user).Returns(Task.FromResult(token));\n\t\t\t_config.OAuthTokenUrl.Returns(\"u\");\n\t\t\t_config.OAuthClientID.Returns(\"c\");\n\t\t\t_config.OAuthClientSecret.Returns(\"s\");\n\t\t\t_oAuth2JwtCallbackProcessor.GetRefreshToken(token, \"u\", \"c\", \"s\").Returns(Task.FromResult(new CallbackResult { IsSuccessful = true, RefreshToken = newToken }));\n\n\t\t\tawait service.AttemptTokenRefresh(user);\n\t\t\t\n\t\t\tawait _oAuth2JwtCallbackProcessor.Received().GetRefreshToken(token, \"u\", \"c\", \"s\");\n\t\t\tawait _userService.Received().UpdateTokenExpiration(user, Arg.Any<DateTime>());\n\t\t\tawait _userService.Received().UpdateRefreshToken(user, newToken);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/PopForums.Test.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<VersionPrefix>22.0.0</VersionPrefix>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<AssemblyName>PopForums.Test</AssemblyName>\n\t\t<PackageId>PopForums.Test</PackageId>\n\t\t<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\PopForums.Mvc\\PopForums.Mvc.csproj\" />\n\t\t<ProjectReference Include=\"..\\PopForums\\PopForums.csproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.4.0\" />\n\t\t<PackageReference Include=\"NSubstitute\" Version=\"5.3.0\" />\n\t\t<PackageReference Include=\"NSubstitute.Analyzers.CSharp\" Version=\"1.0.17\">\n\t\t  <PrivateAssets>all</PrivateAssets>\n\t\t  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n\t\t</PackageReference>\n\t\t<PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\">\n\t\t\t<PrivateAssets>all</PrivateAssets>\n\t\t\t<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n\t\t</PackageReference>\n\t\t<PackageReference Include=\"xunit\" Version=\"2.9.3\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t  <Folder Include=\"Messaging\\\" />\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/PopForums.Test/ScoringGame/AwardCalculatorTests.cs",
    "content": "﻿namespace PopForums.Test.ScoringGame;\n\npublic class AwardCalculatorTests\n{\n\tprivate AwardCalculator GetCalc()\n\t{\n\t\t_awardCalcRepo = Substitute.For<IAwardCalculationQueueRepository>();\n\t\t_eventDefService = Substitute.For<IEventDefinitionService>();\n\t\t_userRepo = Substitute.For<IUserRepository>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\t_awardDefService = Substitute.For<IAwardDefinitionService>();\n\t\t_userAwardService = Substitute.For<IUserAwardService>();\n\t\t_pointLedgerRepo = Substitute.For<IPointLedgerRepository>();\n\t\t_tenantService = Substitute.For<ITenantService>();\n\t\treturn new AwardCalculator(_awardCalcRepo, _eventDefService, _userRepo, _errorLog, _awardDefService, _userAwardService, _pointLedgerRepo, _tenantService);\n\t}\n\n\tprivate IAwardCalculationQueueRepository _awardCalcRepo;\n\tprivate IEventDefinitionService _eventDefService;\n\tprivate IUserRepository _userRepo;\n\tprivate IErrorLog _errorLog;\n\tprivate IAwardDefinitionService _awardDefService;\n\tprivate IUserAwardService _userAwardService;\n\tprivate IPointLedgerRepository _pointLedgerRepo;\n\tprivate ITenantService _tenantService;\n\n\t[Fact]\n\tpublic async Task EnqueueDoesWhatItSaysItShould()\n\t{\n\t\tvar calc = GetCalc();\n\t\tvar user = new User();\n\t\tvar eventDef = new EventDefinition {EventDefinitionID = \"blah\"};\n\t\tvar tenantID = \"t1\";\n\t\t_tenantService.GetTenant().Returns(tenantID);\n\t\tvar payload = new AwardCalculationPayload();\n\t\t_awardCalcRepo.Enqueue(Arg.Do<AwardCalculationPayload>(x => payload = x)).Returns(Task.CompletedTask);\n\t\tawait calc.QueueCalculation(user, eventDef);\n\t\tawait _awardCalcRepo.Received().Enqueue(Arg.Any<AwardCalculationPayload>());\n\t\tAssert.Equal(tenantID, payload.TenantID);\n\t\tAssert.Equal(eventDef.EventDefinitionID, payload.EventDefinitionID);\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessLogsAndDoesNothingWithNullEventDef()\n\t{\n\t\tvar calc = GetCalc();\n\t\tvar user = new User();\n\t\t_eventDefService.GetEventDefinition(Arg.Any<string>()).Returns((EventDefinition) null);\n\t\t_userRepo.GetUser(Arg.Any<int>()).Returns(Task.FromResult(user));\n\t\t_awardCalcRepo.Dequeue().Returns(Task.FromResult(new KeyValuePair<string, int>(\"oih\", user.UserID)));\n\t\tawait calc.ProcessCalculation(null, 0);\n\t\t_errorLog.Received().Log(Arg.Any<Exception>(), ErrorSeverity.Warning);\n\t\tawait _userAwardService.DidNotReceive().IssueAward(Arg.Any<User>(), Arg.Any<AwardDefinition>());\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessNeverCallsIssueIfAwardedAndSingleAward()\n\t{\n\t\tvar calc = GetCalc();\n\t\tvar eventDef = new EventDefinition {EventDefinitionID = \"oi\"};\n\t\tvar user = new User();\n\t\tvar awardDef = new AwardDefinition {AwardDefinitionID = \"sweet\", IsSingleTimeAward = true};\n\t\t_awardCalcRepo.Dequeue().Returns(Task.FromResult(new KeyValuePair<string, int>(eventDef.EventDefinitionID, user.UserID)));\n\t\t_eventDefService.GetEventDefinition(Arg.Any<string>()).Returns(Task.FromResult(eventDef));\n\t\t_userRepo.GetUser(Arg.Any<int>()).Returns(Task.FromResult(user));\n\t\t_awardDefService.GetByEventDefinitionID(eventDef.EventDefinitionID).Returns(Task.FromResult(new List<AwardDefinition> {awardDef}));\n\t\t_userAwardService.IsAwarded(user, awardDef).Returns(Task.FromResult(true));\n\t\tawait calc.ProcessCalculation(eventDef.EventDefinitionID, user.UserID);\n\t\tawait _userAwardService.DidNotReceive().IssueAward(Arg.Any<User>(), Arg.Any<AwardDefinition>());\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessNeverCallsIfEventCountNotHighEnough()\n\t{\n\t\tvar calc = GetCalc();\n\t\tvar eventDef = new EventDefinition { EventDefinitionID = \"oi\" };\n\t\tvar user = new User { UserID = 1 };\n\t\tvar awardDef = new AwardDefinition {AwardDefinitionID = \"sweet\", IsSingleTimeAward = true};\n\t\tvar conditions = new List<AwardCondition>\n\t\t{\n\t\t\tnew AwardCondition { AwardDefinitionID = awardDef.AwardDefinitionID, EventDefinitionID =\"qwerty\", EventCount = 3},\n\t\t\tnew AwardCondition { AwardDefinitionID = awardDef.AwardDefinitionID, EventDefinitionID =\"asdfgh\", EventCount = 5}\n\t\t};\n\t\t_awardCalcRepo.Dequeue().Returns(Task.FromResult(new KeyValuePair<string, int>(eventDef.EventDefinitionID, user.UserID)));\n\t\t_eventDefService.GetEventDefinition(Arg.Any<string>()).Returns(Task.FromResult(eventDef));\n\t\t_userRepo.GetUser(Arg.Any<int>()).Returns(Task.FromResult(user));\n\t\t_awardDefService.GetByEventDefinitionID(eventDef.EventDefinitionID).Returns(Task.FromResult(new List<AwardDefinition> { awardDef }));\n\t\t_userAwardService.IsAwarded(user, awardDef).Returns(Task.FromResult(false));\n\t\t_awardDefService.GetConditions(awardDef.AwardDefinitionID).Returns(Task.FromResult(conditions));\n\t\t_pointLedgerRepo.GetEntryCount(user.UserID, conditions[0].EventDefinitionID).Returns(Task.FromResult(10));\n\t\t_pointLedgerRepo.GetEntryCount(user.UserID, conditions[1].EventDefinitionID).Returns(Task.FromResult(4));\n\t\tawait calc.ProcessCalculation(eventDef.EventDefinitionID, user.UserID);\n\t\tawait _userAwardService.DidNotReceive().IssueAward(Arg.Any<User>(), Arg.Any<AwardDefinition>());\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessIssuesAwardWhenConditionsEqualOrGreater()\n\t{\n\t\tvar calc = GetCalc();\n\t\tvar eventDef = new EventDefinition { EventDefinitionID = \"oi\" };\n\t\tvar user = new User { UserID = 1 };\n\t\tvar awardDef = new AwardDefinition { AwardDefinitionID = \"sweet\", IsSingleTimeAward = true };\n\t\tvar conditions = new List<AwardCondition>\n\t\t{\n\t\t\tnew AwardCondition { AwardDefinitionID = awardDef.AwardDefinitionID, EventDefinitionID =\"qwerty\", EventCount = 3},\n\t\t\tnew AwardCondition { AwardDefinitionID = awardDef.AwardDefinitionID, EventDefinitionID =\"asdfgh\", EventCount = 5}\n\t\t};\n\t\t_awardCalcRepo.Dequeue().Returns(Task.FromResult(new KeyValuePair<string, int>(eventDef.EventDefinitionID, user.UserID)));\n\t\t_eventDefService.GetEventDefinition(Arg.Any<string>()).Returns(Task.FromResult(eventDef));\n\t\t_userRepo.GetUser(Arg.Any<int>()).Returns(Task.FromResult(user));\n\t\t_awardDefService.GetByEventDefinitionID(eventDef.EventDefinitionID).Returns(Task.FromResult(new List<AwardDefinition> { awardDef }));\n\t\t_userAwardService.IsAwarded(user, awardDef).Returns(Task.FromResult(false));\n\t\t_awardDefService.GetConditions(awardDef.AwardDefinitionID).Returns(Task.FromResult(conditions));\n\t\t_pointLedgerRepo.GetEntryCount(user.UserID, conditions[0].EventDefinitionID).Returns(Task.FromResult(10));\n\t\t_pointLedgerRepo.GetEntryCount(user.UserID, conditions[1].EventDefinitionID).Returns(Task.FromResult(5));\n\t\tawait calc.ProcessCalculation(eventDef.EventDefinitionID, user.UserID);\n\t\tawait _userAwardService.Received().IssueAward(Arg.Any<User>(), Arg.Any<AwardDefinition>());\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessIssuesSecondAwardWhenConditionsEqualOrGreater()\n\t{\n\t\tvar calc = GetCalc();\n\t\tvar eventDef = new EventDefinition { EventDefinitionID = \"oi\" };\n\t\tvar user = new User { UserID = 1 };\n\t\tvar firstAwardDef = new AwardDefinition {AwardDefinitionID = \"first\", IsSingleTimeAward = true};\n\t\tvar secondAwardDef = new AwardDefinition { AwardDefinitionID = \"sweet\", IsSingleTimeAward = true };\n\t\tvar conditions = new List<AwardCondition>\n\t\t{\n\t\t\tnew AwardCondition { AwardDefinitionID = secondAwardDef.AwardDefinitionID, EventDefinitionID =\"qwerty\", EventCount = 3},\n\t\t\tnew AwardCondition { AwardDefinitionID = secondAwardDef.AwardDefinitionID, EventDefinitionID =\"asdfgh\", EventCount = 5}\n\t\t};\n\t\t_awardCalcRepo.Dequeue().Returns(Task.FromResult(new KeyValuePair<string, int>(eventDef.EventDefinitionID, user.UserID)));\n\t\t_eventDefService.GetEventDefinition(Arg.Any<string>()).Returns(Task.FromResult(eventDef));\n\t\t_userRepo.GetUser(Arg.Any<int>()).Returns(Task.FromResult(user));\n\t\t_awardDefService.GetByEventDefinitionID(eventDef.EventDefinitionID).Returns(Task.FromResult(new List<AwardDefinition> { firstAwardDef, secondAwardDef }));\n\t\t_userAwardService.IsAwarded(user, secondAwardDef).Returns(Task.FromResult(false));\n\t\t_awardDefService.GetConditions(firstAwardDef.AwardDefinitionID).Returns(Task.FromResult(new List<AwardCondition>()));\n\t\t_awardDefService.GetConditions(secondAwardDef.AwardDefinitionID).Returns(Task.FromResult(conditions));\n\t\t_pointLedgerRepo.GetEntryCount(user.UserID, conditions[0].EventDefinitionID).Returns(Task.FromResult(10));\n\t\t_pointLedgerRepo.GetEntryCount(user.UserID, conditions[1].EventDefinitionID).Returns(Task.FromResult(5));\n\t\tawait calc.ProcessCalculation(eventDef.EventDefinitionID, user.UserID);\n\t\tawait _userAwardService.Received().IssueAward(Arg.Any<User>(), Arg.Any<AwardDefinition>());\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/ScoringGame/AwardCalculatorWorkerTests.cs",
    "content": "using NSubstitute.ExceptionExtensions;\n\nnamespace PopForums.Test.ScoringGame;\n\npublic class AwardCalculatorWorkerTests\n{\n\tprivate IAwardCalculator _calculator;\n\tprivate IAwardCalculationQueueRepository _awardCalculationQueueRepository;\n\tprivate IErrorLog _errorLog;\n\t\n\tprivate IAwardCalculatorWorker GetWorker()\n\t{\n\t\t_calculator = Substitute.For<IAwardCalculator>();\n\t\t_awardCalculationQueueRepository = Substitute.For<IAwardCalculationQueueRepository>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\treturn new AwardCalculatorWorker(_calculator, _awardCalculationQueueRepository, _errorLog);\n\t}\n\t\n\t[Fact]\n\tpublic void DoNothingWhenNoPayload()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_awardCalculationQueueRepository.Dequeue().Returns(new KeyValuePair <string, int>(null, 1));\n\t\t\n\t\tworker.Execute();\n\n\t\t_calculator.DidNotReceiveWithAnyArgs().ProcessCalculation(Arg.Any<string>(), Arg.Any<int>());\n\t\t_errorLog.DidNotReceiveWithAnyArgs().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\n\t[Fact]\n\tpublic void CallProcessPayloadWithPayloadValues()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_awardCalculationQueueRepository.Dequeue().Returns(new KeyValuePair <string, int>(\"key\", 1));\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_calculator.Received().ProcessCalculation(\"key\", 1);\n\t\t_errorLog.DidNotReceiveWithAnyArgs().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\t\n\t[Fact]\n\tpublic void LogWhenDequeueThrows()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_awardCalculationQueueRepository.Dequeue().ThrowsAsync(new Exception());\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_calculator.DidNotReceiveWithAnyArgs().ProcessCalculation(Arg.Any<string>(), Arg.Any<int>());\n\t\t_errorLog.Received().Log(Arg.Any<Exception>(), ErrorSeverity.Error);\n\t}\n\n\t[Fact]\n\tpublic void LogWhenProcessCalculationThrows()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_awardCalculationQueueRepository.Dequeue().Returns(new KeyValuePair <string, int>(\"key\", 1));\n\t\t_calculator.When(x => x.ProcessCalculation(\"key\", 1)).Throw(new Exception());\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_errorLog.Received().Log(Arg.Any<Exception>(), ErrorSeverity.Error);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/ScoringGame/AwardDefinitionServiceTests.cs",
    "content": "﻿namespace PopForums.Test.ScoringGame;\n\npublic class AwardDefinitionServiceTests\n{\n\tpublic AwardDefinitionService GetService()\n\t{\n\t\t_awardDefRepo = Substitute.For<IAwardDefinitionRepository>();\n\t\t_awardConditionRepo = Substitute.For<IAwardConditionRepository>();\n\t\treturn new AwardDefinitionService(_awardDefRepo, _awardConditionRepo);\n\t}\n\n\tprivate IAwardDefinitionRepository _awardDefRepo;\n\tprivate IAwardConditionRepository _awardConditionRepo;\n\n\t[Fact]\n\tpublic void CreateMapsObjectToRepo()\n\t{\n\t\tvar awardDef = new AwardDefinition {AwardDefinitionID = \"blah\", Title = \"title\", Description = \"desc\", IsSingleTimeAward = true};\n\t\tvar service = GetService();\n\t\tservice.Create(awardDef);\n\t\t_awardDefRepo.Received().Create(awardDef.AwardDefinitionID, awardDef.Title, awardDef.Description, awardDef.IsSingleTimeAward);\n\t}\n\n\t[Fact]\n\tpublic async Task SaveConditionsDeletesOldOnes()\n\t{\n\t\tvar awardDef = new AwardDefinition {AwardDefinitionID = \"awarddef\"};\n\t\tvar service = GetService();\n\t\tawait service.SaveConditions(awardDef, new List<AwardCondition>());\n\t\tawait _awardConditionRepo.Received().DeleteConditions(awardDef.AwardDefinitionID);\n\t}\n\n\t[Fact]\n\tpublic async Task SaveConditionsSetsAllAwardDefIDs()\n\t{\n\t\tvar awardDef = new AwardDefinition { AwardDefinitionID = \"awarddef\" };\n\t\tvar list = new List<AwardCondition> { new AwardCondition { AwardDefinitionID = \"bad\" }, new AwardCondition { AwardDefinitionID = \"toobad\" } };\n\t\tvar savingList = new List<AwardCondition>();\n\t\tvar service = GetService();\n\t\tawait _awardConditionRepo.SaveConditions(Arg.Do<List<AwardCondition>>(x => savingList = x));\n\t\tawait service.SaveConditions(awardDef, list);\n\t\tAssert.Equal(savingList[0].AwardDefinitionID, awardDef.AwardDefinitionID);\n\t\tAssert.Equal(savingList[1].AwardDefinitionID, awardDef.AwardDefinitionID);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/ScoringGame/EventDefintionServiceTests.cs",
    "content": "﻿namespace PopForums.Test.ScoringGame;\n\npublic class EventDefintionServiceTests\n{\n\tprivate EventDefinitionService GetService()\n\t{\n\t\t_eventDefRepo = Substitute.For<IEventDefinitionRepository>();\n\t\t_awardConditionRepo = Substitute.For<IAwardConditionRepository>();\n\t\treturn new EventDefinitionService(_eventDefRepo, _awardConditionRepo);\n\t}\n\n\tprivate IEventDefinitionRepository _eventDefRepo;\n\tprivate IAwardConditionRepository _awardConditionRepo;\n\n\t[Fact]\n\tpublic async Task GetReturnsFromRepo()\n\t{\n\t\tvar service = GetService();\n\t\tvar def = new EventDefinition {EventDefinitionID = \"whatevs\", PointValue = 2, Description = \"stuff\"};\n\t\t_eventDefRepo.Get(def.EventDefinitionID).Returns(Task.FromResult(def));\n\t\tvar result = await service.GetEventDefinition(def.EventDefinitionID);\n\t\tAssert.Same(def, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetStaticPostVoteReturnsStaticObject()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = await service.GetEventDefinition(EventDefinitionService.StaticEventIDs.PostVote);\n\t\tAssert.Same(EventDefinitionService.StaticEvents[EventDefinitionService.StaticEventIDs.PostVote], result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetAllMergesStaticWithRepo()\n\t{\n\t\tvar service = GetService();\n\t\tvar list = new List<EventDefinition> {new() {EventDefinitionID = \"AAA\"}, new() {EventDefinitionID = \"ZZZ\"}};\n\t\t_eventDefRepo.GetAll().Returns(Task.FromResult(list));\n\t\tvar result = await service.GetAll();\n\t\tAssert.Equal(7, result.Count);\n\t\tAssert.True(result.Count(x => x.EventDefinitionID == \"AAA\") == 1);\n\t\tAssert.True(result.Count(x => x.EventDefinitionID == \"ZZZ\") == 1);\n\t\tAssert.True(result.Count(x => x.EventDefinitionID == EventDefinitionService.StaticEventIDs.NewPost) == 1);\n\t\tAssert.True(result.Count(x => x.EventDefinitionID == EventDefinitionService.StaticEventIDs.NewTopic) == 1);\n\t\tAssert.True(result.Count(x => x.EventDefinitionID == EventDefinitionService.StaticEventIDs.PostVote) == 1);\n\t}\n\t\n\t[Fact]\n\tpublic async Task GetAllMergesAndOrders()\n\t{\n\t\tvar service = GetService();\n\t\tvar list = new List<EventDefinition> { new() { EventDefinitionID = \"AAA\" }, new() { EventDefinitionID = \"ZZZ\" } };\n\t\t_eventDefRepo.GetAll().Returns(Task.FromResult(list));\n\t\tvar result = await service.GetAll();\n\t\tAssert.Equal(7, result.Count);\n\t\tAssert.Equal(\"AAA\", result[0].EventDefinitionID);\n\t\tAssert.Equal(EventDefinitionService.StaticEventIDs.NewPost, result[1].EventDefinitionID);\n\t\tAssert.Equal(EventDefinitionService.StaticEventIDs.NewTopic, result[2].EventDefinitionID);\n\t\tAssert.Equal(EventDefinitionService.StaticEventIDs.PostVote, result[3].EventDefinitionID);\n\t\tAssert.Equal(EventDefinitionService.StaticEventIDs.PostVoteUndo, result[4].EventDefinitionID);\n\t\tAssert.Equal(\"ZZZ\", result[6].EventDefinitionID);\n\t}\n\n\t[Fact]\n\tpublic async Task CreatePassesToRepo()\n\t{\n\t\tvar service = GetService();\n\t\tvar eventDef = new EventDefinition();\n\t\tawait service.Create(eventDef);\n\t\tawait _eventDefRepo.Received().Create(eventDef);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteCallsEventDefRepoAndAwardConditionRepo()\n\t{\n\t\tvar service = GetService();\n\t\tconst string eventDefID = \"ohnoes!\";\n\t\tawait service.Delete(eventDefID);\n\t\t_eventDefRepo.Received().Delete(eventDefID);\n\t\tawait _awardConditionRepo.Received().DeleteConditionsByEventDefinitionID(eventDefID);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/ScoringGame/EventPublisherTests.cs",
    "content": "﻿namespace PopForums.Test.ScoringGame;\n\npublic class EventPublisherTests\n{\n\tprivate EventPublisher GetPublisher()\n\t{\n\t\t_eventDefService = Substitute.For<IEventDefinitionService>();\n\t\t_pointLedgerRepo = Substitute.For<IPointLedgerRepository>();\n\t\t_feedService = Substitute.For<IFeedService>();\n\t\t_awardCalc = Substitute.For<IAwardCalculator>();\n\t\t_profileService = Substitute.For<IProfileService>();\n\t\treturn new EventPublisher(_eventDefService, _pointLedgerRepo, _feedService, _awardCalc, _profileService);\n\t}\n\n\tprivate IEventDefinitionService _eventDefService;\n\tprivate IPointLedgerRepository _pointLedgerRepo;\n\tprivate IFeedService _feedService;\n\tprivate IAwardCalculator _awardCalc;\n\tprivate IProfileService _profileService;\n\n\t[Fact]\n\tpublic async Task ProcessEventPublishesToLedger()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar eventDef = new EventDefinition {EventDefinitionID = \"blah\", PointValue = 42};\n\t\tconst string message = \"msg\";\n\t\tvar publisher = GetPublisher();\n\t\t_eventDefService.GetEventDefinition(eventDef.EventDefinitionID).Returns(Task.FromResult(eventDef));\n\t\tvar entry = new PointLedgerEntry();\n\t\tawait _pointLedgerRepo.RecordEntry(Arg.Do<PointLedgerEntry>(x => entry = x));\n\t\tawait publisher.ProcessEvent(message, user, eventDef.EventDefinitionID, false);\n\t\tAssert.Equal(user.UserID, entry.UserID);\n\t\tAssert.Equal(eventDef.EventDefinitionID, entry.EventDefinitionID);\n\t\tAssert.Equal(eventDef.PointValue, entry.Points);\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessEventPublishesToFeedService()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar eventDef = new EventDefinition { EventDefinitionID = \"blah\", PointValue = 42, IsPublishedToFeed = true };\n\t\tconst string message = \"msg\";\n\t\tvar publisher = GetPublisher();\n\t\t_eventDefService.GetEventDefinition(eventDef.EventDefinitionID).Returns(Task.FromResult(eventDef));\n\t\tawait publisher.ProcessEvent(message, user, eventDef.EventDefinitionID, false);\n\t\tawait _feedService.Received().PublishToFeed(user, message, eventDef.PointValue, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessEventDoesNotPublishToFeedServiceWhenEventDefSaysNo()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar eventDef = new EventDefinition { EventDefinitionID = \"blah\", PointValue = 42, IsPublishedToFeed = false };\n\t\tconst string message = \"msg\";\n\t\tvar publisher = GetPublisher();\n\t\t_eventDefService.GetEventDefinition(eventDef.EventDefinitionID).Returns(Task.FromResult(eventDef));\n\t\tawait publisher.ProcessEvent(message, user, eventDef.EventDefinitionID, false);\n\t\tawait _feedService.DidNotReceive().PublishToFeed(user, message, eventDef.PointValue, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessEventCallsCalculator()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar eventDef = new EventDefinition { EventDefinitionID = \"blah\", PointValue = 42 };\n\t\tvar publisher = GetPublisher();\n\t\t_eventDefService.GetEventDefinition(eventDef.EventDefinitionID).Returns(Task.FromResult(eventDef));\n\t\tawait publisher.ProcessEvent(\"msg\", user, eventDef.EventDefinitionID, false);\n\t\tawait _awardCalc.Received().QueueCalculation(user, eventDef);\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessEventUpdatesProfilePointTotal()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar eventDef = new EventDefinition { EventDefinitionID = \"blah\", PointValue = 42 };\n\t\tvar publisher = GetPublisher();\n\t\t_eventDefService.GetEventDefinition(eventDef.EventDefinitionID).Returns(Task.FromResult(eventDef));\n\t\tawait publisher.ProcessEvent(\"msg\", user, eventDef.EventDefinitionID, false);\n\t\tawait _profileService.Received().UpdatePointTotal(user);\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessManualEventPublishesToLedger()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tconst string message = \"msg\";\n\t\tconst int points = 252;\n\t\tvar publisher = GetPublisher();\n\t\tvar entry = new PointLedgerEntry();\n\t\tawait _pointLedgerRepo.RecordEntry(Arg.Do<PointLedgerEntry>(x => entry = x));\n\t\tawait publisher.ProcessManualEvent(message, user, points);\n\t\tAssert.Equal(user.UserID, entry.UserID);\n\t\tAssert.Equal(\"Manual\", entry.EventDefinitionID);\n\t\tAssert.Equal(points, entry.Points);\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessManualEventPublishesToFeedService()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tconst string message = \"msg\";\n\t\tconst int points = 252;\n\t\tvar publisher = GetPublisher();\n\t\tawait publisher.ProcessManualEvent(message, user, points);\n\t\tawait _feedService.Received().PublishToFeed(user, message, points, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task ProcessManualEventUpdatesProfilePointTotal()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar publisher = GetPublisher();\n\t\tawait publisher.ProcessManualEvent(\"msg\", user, 252);\n\t\tawait _profileService.Received().UpdatePointTotal(user);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/ScoringGame/FeedServiceTests.cs",
    "content": "namespace PopForums.Test.ScoringGame;\n\npublic class FeedServiceTests\n{\n\tprivate FeedService GetService()\n\t{\n\t\t_feedRepo = Substitute.For<IFeedRepository>();\n\t\t_broker = Substitute.For<IBroker>();\n\t\treturn new FeedService(_feedRepo, _broker);\n\t}\n\n\tprivate IFeedRepository _feedRepo;\n\tprivate IBroker _broker;\n\n\t[Fact]\n\tpublic async Task PublishSavesToRepo()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tconst string msg = \"oiehgfoih\";\n\t\tconst int points = 5352;\n\t\tvar timeStamp = new DateTime(2000, 1, 1);\n\t\tawait service.PublishToFeed(user, msg, points, timeStamp);\n\t\tawait _feedRepo.Received().PublishEvent(user.UserID, msg, points, timeStamp);\n\t}\n\n\t[Fact]\n\tpublic async Task PublishDeletesOlderThan50()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar timeStamp = new DateTime(2000, 1, 1);\n\t\tvar cutOff = new DateTime(1999, 2, 2);\n\t\tconst int points = 5352;\n\t\t_feedRepo.GetOldestTime(user.UserID, 50).Returns(Task.FromResult(cutOff));\n\t\tawait service.PublishToFeed(user, \"whatevs\", points, timeStamp);\n\t\tawait _feedRepo.Received().DeleteOlderThan(user.UserID, cutOff);\n\t}\n\n\t[Fact]\n\tpublic async Task PublishDoesNothingIfUserIsNull()\n\t{\n\t\tvar service = GetService();\n\t\tawait service.PublishToFeed(null, String.Empty, 423, new DateTime());\n\t\tawait _feedRepo.DidNotReceive().PublishEvent(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task GetFeedGets50ItemsMaxFromRepo()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar list = new List<FeedEvent>();\n\t\t_feedRepo.GetFeed(user.UserID, 50).Returns(Task.FromResult(list));\n\t\tvar result = await service.GetFeed(user);\n\t\tAssert.Same(result, list);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/ScoringGame/UserAwardServiceTests.cs",
    "content": "﻿namespace PopForums.Test.ScoringGame;\n\npublic class UserAwardServiceTests\n{\n\tpublic UserAwardService GetService()\n\t{\n\t\t_userAwardRepo = Substitute.For<IUserAwardRepository>();\n\t\t_notificationTunnel = Substitute.For<INotificationTunnel>();\n\t\t_tenantService = Substitute.For<ITenantService>();\n\t\treturn new UserAwardService(_userAwardRepo, _notificationTunnel, _tenantService);\n\t}\n\n\tprivate IUserAwardRepository _userAwardRepo;\n\tprivate INotificationTunnel _notificationTunnel;\n\tprivate ITenantService _tenantService;\n\n\t[Fact]\n\tpublic async Task IssueMapsFieldsToRepoCall()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar awardDef = new AwardDefinition {AwardDefinitionID = \"blah\", Description = \"desc\", Title = \"title\", IsSingleTimeAward = true};\n\t\tvar service = GetService();\n\t\tawait service.IssueAward(user, awardDef);\n\t\tawait _userAwardRepo.Received().IssueAward(user.UserID, awardDef.AwardDefinitionID, awardDef.Title, awardDef.Description, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task IsAwardedMapsAndReturnsRightValue()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar awardDef = new AwardDefinition { AwardDefinitionID = \"blah\" };\n\t\tvar service = GetService();\n\t\t_userAwardRepo.IsAwarded(user.UserID, awardDef.AwardDefinitionID).Returns(Task.FromResult(true));\n\t\tvar result = await service.IsAwarded(user, awardDef);\n\t\tAssert.True(result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetAwardsMapsUserIDAndReturnsList()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar list = new List<UserAward>();\n\t\tvar service = GetService();\n\t\t_userAwardRepo.GetAwards(user.UserID).Returns(Task.FromResult(list));\n\t\tvar result = await service.GetAwards(user);\n\t\tAssert.Same(list, result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/BanServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class BanServiceTests\n{\n\tprivate IBanRepository _banRepo;\n\n\tprivate IBanService GetService()\n\t{\n\t\t_banRepo = Substitute.For<IBanRepository>();\n\t\treturn new BanService(_banRepo);\n\t}\n\n\t[Fact]\n\tpublic async Task IPTrimmedOnSave()\n\t{\n\t\tvar service = GetService();\n\t\tawait service.BanIP(\"  1.1.1.1  \");\n\t\tawait _banRepo.Received().BanIP(\"1.1.1.1\");\n\t}\n\n\t[Fact]\n\tpublic async Task EmailTrimmedOnSave()\n\t{\n\t\tvar service = GetService();\n\t\tawait service.BanEmail(\"  a@b.com  \");\n\t\tawait _banRepo.Received().BanEmail(\"a@b.com\");\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/CategoryServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class CategoryServiceTests\n{\n\tprivate ICategoryRepository _mockCategoryRepo;\n\tprivate IForumRepository _mockForumRepo;\n\n\tprivate ICategoryService GetService()\n\t{\n\t\t_mockCategoryRepo = Substitute.For<ICategoryRepository>();\n\t\t_mockForumRepo = Substitute.For<IForumRepository>();\n\t\tvar service = new CategoryService(_mockCategoryRepo, _mockForumRepo);\n\t\treturn service;\n\t}\n\n\t[Fact]\n\tpublic async Task GetAll()\n\t{\n\t\tvar service = GetService();\n\t\tvar allCats = new List<Category>();\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(allCats));\n\t\tvar result = await service.GetAll();\n\t\tAssert.Same(allCats, result);\n\t\tawait _mockCategoryRepo.Received().GetAll();\n\t}\n\n\t[Fact]\n\tpublic async Task Create()\n\t{\n\t\tconst string newTitle = \"new category\";\n\t\tvar cat1 = new Category { CategoryID = 123, SortOrder = 0 };\n\t\tvar cat2 = new Category { CategoryID = 456, SortOrder = 2 };\n\t\tvar cat3 = new Category { CategoryID = 789, SortOrder = 4 };\n\t\tvar cat4 = new Category { CategoryID = 1000, SortOrder = 6 };\n\t\tvar newCat = new Category { CategoryID = 999, Title = newTitle, SortOrder = -2};\n\t\tvar cats = new List<Category> { cat1, cat2, cat3, cat4, newCat };\n\t\tvar service = GetService();\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(cats));\n\t\t_mockCategoryRepo.Create(newTitle, -2).Returns(Task.FromResult(newCat));\n\t\tvar result = await service.Create(newTitle);\n\t\tAssert.Equal(0, result.SortOrder);\n\t\tAssert.Equal(999, result.CategoryID);\n\t\tAssert.Equal(newTitle, result.Title);\n\t\tawait _mockCategoryRepo.Received().Create(newTitle, -2);\n\t\tAssert.Equal(0, newCat.SortOrder);\n\t\tAssert.Equal(2, cat1.SortOrder);\n\t\tAssert.Equal(4, cat2.SortOrder);\n\t\tAssert.Equal(6, cat3.SortOrder);\n\t\tAssert.Equal(8, cat4.SortOrder);\n\t}\n\n\t[Fact]\n\tpublic void Delete()\n\t{\n\t\tvar service = GetService();\n\t\tvar cat = new Category { CategoryID = 123 };\n\t\tservice.Delete(cat);\n\t\t_mockCategoryRepo.Received().Delete(cat.CategoryID);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteByIdThrowsIfNotFound()\n\t{\n\t\tvar service = GetService();\n\t\t_mockCategoryRepo.Get(Arg.Any<int>()).Returns(Task.FromResult((Category)null));\n\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.Delete(1));\n\t}\n\n\t[Fact]\n\tpublic void DeleteResetsForumCatIDsToNull()\n\t{\n\t\tvar service = GetService();\n\t\tvar cat = new Category { CategoryID = 123 };\n\t\tvar f1 = new Forum { ForumID = 1, CategoryID = cat.CategoryID };\n\t\tvar f2 = new Forum { ForumID = 2, CategoryID = cat.CategoryID };\n\t\tvar f3 = new Forum { ForumID = 3, CategoryID = 456 };\n\t\tvar forums = new List<Forum> {f1, f2, f3};\n\t\t_mockForumRepo.GetAll().Returns(forums);\n\t\tservice.Delete(cat);\n\t\t_mockForumRepo.Received().UpdateCategoryAssociation(1, null);\n\t\t_mockForumRepo.Received().UpdateCategoryAssociation(2, null);\n\t\t_mockForumRepo.DidNotReceive().UpdateCategoryAssociation(3, null);\n\t}\n\n\t[Fact]\n\tpublic void UpdateTitle()\n\t{\n\t\tvar savedCategory = new Category { CategoryID = 789 };\n\t\tvar service = GetService();\n\t\tvar cat = new Category { CategoryID = 123, Title = \"old\", SortOrder = 456 };\n\t\t_mockCategoryRepo.Update(Arg.Do<Category>(x => savedCategory = x));\n\t\tservice.UpdateTitle(cat, \"new\");\n\t\t_mockCategoryRepo.Received().Update(Arg.Any<Category>());\n\t\tAssert.Equal(\"new\", savedCategory.Title);\n\t\tAssert.Equal(123, savedCategory.CategoryID);\n\t\tAssert.Equal(456, savedCategory.SortOrder);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTitleByIdThrowsIfNotFound()\n\t{\n\t\tvar service = GetService();\n\t\t_mockCategoryRepo.Get(Arg.Any<int>()).Returns((Category)null);\n\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.UpdateTitle(1, \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task MoveUp()\n\t{\n\t\tvar cat1 = new Category { CategoryID = 123, SortOrder = 0};\n\t\tvar cat2 = new Category { CategoryID = 456, SortOrder = 2};\n\t\tvar cat3 = new Category { CategoryID = 789, SortOrder = 4};\n\t\tvar cat4 = new Category { CategoryID = 1000, SortOrder = 6};\n\t\tvar cats = new List<Category> { cat1, cat2, cat3, cat4 };\n\t\tvar service = GetService();\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(cats));\n\t\tawait service.MoveCategoryUp(cat3);\n\t\tawait _mockCategoryRepo.Received().GetAll();\n\t\tawait _mockCategoryRepo.Received(4).Update(Arg.Any<Category>());\n\t\tawait _mockCategoryRepo.Received().Update(cat1);\n\t\tawait _mockCategoryRepo.Received().Update(cat2);\n\t\tawait _mockCategoryRepo.Received().Update(cat3);\n\t\tawait _mockCategoryRepo.Received().Update(cat4);\n\t\tAssert.Equal(0, cat1.SortOrder);\n\t\tAssert.Equal(2, cat3.SortOrder);\n\t\tAssert.Equal(4, cat2.SortOrder);\n\t\tAssert.Equal(6, cat4.SortOrder);\n\t}\n\n\t[Fact]\n\tpublic async Task MoveDown()\n\t{\n\t\tvar cat1 = new Category { CategoryID = 123, SortOrder = 0 };\n\t\tvar cat2 = new Category { CategoryID = 456, SortOrder = 2 };\n\t\tvar cat3 = new Category { CategoryID = 789, SortOrder = 4 };\n\t\tvar cat4 = new Category { CategoryID = 1000, SortOrder = 6 };\n\t\tvar cats = new List<Category> { cat1, cat2, cat3, cat4 };\n\t\tvar service = GetService();\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(cats));\n\t\tawait service.MoveCategoryDown(cat3);\n\t\tawait _mockCategoryRepo.Received().GetAll();\n\t\tawait _mockCategoryRepo.Received(4).Update(Arg.Any<Category>());\n\t\tawait _mockCategoryRepo.Received().Update(cat1);\n\t\tawait _mockCategoryRepo.Received().Update(cat2);\n\t\tawait _mockCategoryRepo.Received().Update(cat3);\n\t\tawait _mockCategoryRepo.Received().Update(cat4);\n\t\tAssert.Equal(0, cat1.SortOrder);\n\t\tAssert.Equal(2, cat2.SortOrder);\n\t\tAssert.Equal(4, cat4.SortOrder);\n\t\tAssert.Equal(6, cat3.SortOrder);\n\t}\n\n\t[Fact]\n\tpublic async Task MoveUpByIdThrowsIfNotFound()\n\t{\n\t\tvar service = GetService();\n\t\t_mockCategoryRepo.Get(Arg.Any<int>()).Returns((Category)null);\n\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.MoveCategoryUp(1));\n\t}\n\n\t[Fact]\n\tpublic async Task MoveDownByIdThrowsIfNotFound()\n\t{\n\t\tvar service = GetService();\n\t\t_mockCategoryRepo.Get(Arg.Any<int>()).Returns((Category)null);\n\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.MoveCategoryDown(1));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/ClaimsToRoleMapperTests.cs",
    "content": "using System.Security.Claims;\n\nnamespace PopForums.Test.Services;\n\npublic class ClaimsToRoleMapperTests\n{\n\tprivate IConfig _config;\n\tprivate IRoleRepository _roleRepo;\n\t\n\tprivate ClaimsToRoleMapper GetService()\n\t{\n\t\t_config = Substitute.For<IConfig>();\n\t\t_roleRepo = Substitute.For<IRoleRepository>();\n\t\treturn new ClaimsToRoleMapper(_config, _roleRepo);\n\t}\n\n\tpublic class MapRoles : ClaimsToRoleMapperTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task NoMappingWithNoClaims()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_config.OAuthAdminClaimType.Returns((string)null);\n\t\t\t_config.OAuthAdminClaimValue.Returns((string)null);\n\t\t\tvar user = new User { Roles = new List<string>(), UserID = 123 };\n\t\t\tvar claims = new List<Claim>();\n\t\t\tvar savedRoles = Array.Empty<string>();\n\t\t\tawait _roleRepo.ReplaceUserRoles(user.UserID, Arg.Do<string[]>(x => savedRoles = x));\n\n\t\t\tawait service.MapRoles(user, claims);\n\n\t\t\tAssert.Empty(savedRoles);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task NoMappingWithNoMatchingClaims()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_config.OAuthAdminClaimType.Returns(\"iowfhwe\");\n\t\t\t_config.OAuthAdminClaimValue.Returns(\"efoijh\");\n\t\t\t_config.OAuthModeratorClaimType.Returns(\"iowfhwe\");\n\t\t\t_config.OAuthModeratorClaimValue.Returns(\"efoijh\");\n\t\t\tvar user = new User { Roles = new List<string>(), UserID = 123 };\n\t\t\tvar claims = new List<Claim>();\n\t\t\tvar savedRoles = Array.Empty<string>();\n\t\t\tawait _roleRepo.ReplaceUserRoles(user.UserID, Arg.Do<string[]>(x => savedRoles = x));\n\n\t\t\tawait service.MapRoles(user, claims);\n\n\t\t\tAssert.Empty(savedRoles);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task NoMappingWithNoMatchingClaimsValues()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_config.OAuthAdminClaimType.Returns(\"admin\");\n\t\t\t_config.OAuthAdminClaimValue.Returns(\"yes\");\n\t\t\t_config.OAuthModeratorClaimType.Returns(\"mod\");\n\t\t\t_config.OAuthModeratorClaimValue.Returns(\"yes\");\n\t\t\tvar user = new User { Roles = new List<string>(), UserID = 123 };\n\t\t\tvar claims = new List<Claim>\n\t\t\t{\n\t\t\t\tnew (\"admin\", \"no\"),\n\t\t\t\tnew (\"mod\", \"no\")\n\t\t\t};\n\t\t\tvar savedRoles = Array.Empty<string>();\n\t\t\tawait _roleRepo.ReplaceUserRoles(user.UserID, Arg.Do<string[]>(x => savedRoles = x));\n\n\t\t\tawait service.MapRoles(user, claims);\n\n\t\t\tAssert.Empty(savedRoles);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task AdminNameNoValueMapsAdminRole()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_config.OAuthAdminClaimType.Returns(\"adminclaim\");\n\t\t\t_config.OAuthAdminClaimValue.Returns((string)null);\n\t\t\tvar user = new User { Roles = new List<string>(), UserID = 123 };\n\t\t\tvar claims = new List<Claim>\n\t\t\t{\n\t\t\t\tnew (\"adminclaim\", string.Empty)\n\t\t\t};\n\t\t\tvar savedRoles = Array.Empty<string>();\n\t\t\tawait _roleRepo.ReplaceUserRoles(user.UserID, Arg.Do<string[]>(x => savedRoles = x));\n\n\t\t\tawait service.MapRoles(user, claims);\n\n\t\t\tAssert.Contains(PermanentRoles.Admin, savedRoles);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task AdminNameWithValueMapsAdminRole()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_config.OAuthAdminClaimType.Returns(\"adminclaim\");\n\t\t\t_config.OAuthAdminClaimValue.Returns(\"adminvalue\");\n\t\t\tvar user = new User { Roles = new List<string>(), UserID = 123 };\n\t\t\tvar claims = new List<Claim>\n\t\t\t{\n\t\t\t\tnew (\"adminclaim\", \"adminvalue\")\n\t\t\t};\n\t\t\tvar savedRoles = Array.Empty<string>();\n            await _roleRepo.ReplaceUserRoles(user.UserID, Arg.Do<string[]>(x => savedRoles = x));\n\n\t\t\tawait service.MapRoles(user, claims);\n\n\t\t\tAssert.Contains(PermanentRoles.Admin, savedRoles);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ModNameNoValueMapsModRole()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_config.OAuthModeratorClaimType.Returns(\"modclaim\");\n\t\t\t_config.OAuthModeratorClaimValue.Returns((string)null);\n\t\t\tvar user = new User { Roles = new List<string>(), UserID = 123 };\n\t\t\tvar claims = new List<Claim>\n\t\t\t{\n\t\t\t\tnew (\"modclaim\", string.Empty)\n\t\t\t};\n\t\t\tvar savedRoles = Array.Empty<string>();\n\t\t\tawait _roleRepo.ReplaceUserRoles(user.UserID, Arg.Do<string[]>(x => savedRoles = x));\n\n\t\t\tawait service.MapRoles(user, claims);\n\n\t\t\tAssert.Contains(PermanentRoles.Moderator, savedRoles);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ModNameWithValueMapsModRole()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_config.OAuthAdminClaimType.Returns(\"modclaim\");\n\t\t\t_config.OAuthAdminClaimValue.Returns(\"modvalue\");\n\t\t\tvar user = new User { Roles = new List<string>(), UserID = 123 };\n\t\t\tvar claims = new List<Claim>\n\t\t\t{\n\t\t\t\tnew (\"modclaim\", \"modvalue\")\n\t\t\t};\n\t\t\tvar savedRoles = Array.Empty<string>();\n\t\t\tawait _roleRepo.ReplaceUserRoles(user.UserID, Arg.Do<string[]>(x => savedRoles = x));\n\n\t\t\tawait service.MapRoles(user, claims);\n\n\t\t\tAssert.Contains(PermanentRoles.Admin, savedRoles);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/CloseAgedTopicsWorkerTests.cs",
    "content": "using NSubstitute.ExceptionExtensions;\n\nnamespace PopForums.Test.Services;\n\npublic class CloseAgedTopicsWorkerTests\n{\n\tprivate ITopicService _topicService;\n\tprivate IErrorLog _errorLog;\n\n\tprivate CloseAgedTopicsWorker GetWorker()\n\t{\n\t\t_topicService = Substitute.For<ITopicService>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\treturn new CloseAgedTopicsWorker(_topicService, _errorLog);\n\t}\n\n\t[Fact]\n\tpublic void NoErrorNoLog()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_topicService.CloseAgedTopics().Returns(Task.CompletedTask);\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_errorLog.DidNotReceive().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\n\t[Fact]\n\tpublic void LogWhenThrows()\n\t{\n\t\tvar worker = GetWorker(); \n\t\t_topicService.CloseAgedTopics().ThrowsAsync<Exception>();\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_errorLog.Received().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/FavoriteTopicServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class FavoriteTopicServiceTests\n{\n\tprivate IFavoriteTopicsRepository _mockFaveRepo;\n\tprivate ISettingsManager _mockSettingsManager;\n\n\tprivate FavoriteTopicService GetService()\n\t{\n\t\t_mockFaveRepo = Substitute.For<IFavoriteTopicsRepository>();\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\treturn new FavoriteTopicService(_mockSettingsManager, _mockFaveRepo);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsFromRepo()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tvar settings = new Settings { TopicsPerPage = 20 };\n\t\t_mockSettingsManager.Current.Returns(settings);\n\t\tvar list = new List<Topic>();\n\t\t_mockFaveRepo.GetFavoriteTopics(user.UserID, 1, 20).Returns(Task.FromResult(list));\n\t\tvar result = await service.GetTopics(user, 1);\n\t\tAssert.Same(list, result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task AddFaveTopic()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456 };\n\t\tawait service.AddFavoriteTopic(user, topic);\n\t\tawait _mockFaveRepo.Received().AddFavoriteTopic(user.UserID, topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task RemoveFaveTopic()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456 };\n\t\tawait service.RemoveFavoriteTopic(user, topic);\n\t\tawait _mockFaveRepo.Received().RemoveFavoriteTopic(user.UserID, topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsStartRowCalcd()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tvar settings = new Settings { TopicsPerPage = 20 };\n\t\t_mockSettingsManager.Current.Returns(settings);\n\t\tvar result = await service.GetTopics(user, 3);\n\t\tawait _mockFaveRepo.Received().GetFavoriteTopics(user.UserID, 41, 20);\n\t\tresult.Item2.PageSize = settings.TopicsPerPage;\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/ForumPermissionServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class ForumPermissionServiceTests\n{\n\tprivate ForumPermissionService GetService()\n\t{\n\t\t_mockForumRepo = Substitute.For<IForumRepository>();\n\t\treturn new ForumPermissionService(_mockForumRepo);\n\t}\n\n\tprivate IForumRepository _mockForumRepo;\n\n\tprivate User GetUser()\n\t{\n\t\tvar user = Models.UserTest.GetTestUser();\n\t\tuser.Roles = new List<string>();\n\t\treturn user;\n\t}\n\n\t[Fact]\n\tpublic async Task NoViewRestrictionWithUser()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, GetUser());\n\t\tAssert.True(permission.UserCanView);\n\t\tAssert.Empty(permission.DenialReason);\n\t}\n\n\t[Fact]\n\tpublic async Task NoViewRestrictionWithoutUser()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, null);\n\t\tAssert.True(permission.UserCanView);\n\t}\n\n\t[Fact]\n\tpublic async Task ViewRestrictionUserNotInRole()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string> { \"blah\" }));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, GetUser());\n\t\tAssert.False(permission.UserCanView);\n\t}\n\n\t[Fact]\n\tpublic async Task ViewRestrictionUserCantPostEither()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string> { \"blah\" }));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, GetUser());\n\t\tAssert.False(permission.UserCanView);\n\t\tAssert.False(permission.UserCanPost);\n\t}\n\n\t[Fact]\n\tpublic async Task ViewRestrictionNoUser()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string> { \"blah\" }));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, null);\n\t\tAssert.False(permission.UserCanView);\n\t}\n\n\t[Fact]\n\tpublic async Task ViewRestrictionUserInRole()\n\t{\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(\"blah\");\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string> { \"blah\" }));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user);\n\t\tAssert.True(permission.UserCanView);\n\t}\n\n\t[Fact]\n\tpublic async Task PostRestrictionNoUser()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string> { \"blah\" }));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, null);\n\t\tAssert.False(permission.UserCanPost);\n\t}\n\n\t[Fact]\n\tpublic async Task PostRestrictionUserInRole()\n\t{\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(\"blah\");\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string> { \"blah\" }));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user);\n\t\tAssert.True(permission.UserCanPost);\n\t\tAssert.Empty(permission.DenialReason);\n\t}\n\n\t[Fact]\n\tpublic async Task PostRestrictionUserNotApproved()\n\t{\n\t\tvar user = GetUser();\n\t\tuser.IsApproved = false;\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user);\n\t\tAssert.False(permission.UserCanPost);\n\t\tAssert.NotEmpty(permission.DenialReason);\n\t}\n\n\t[Fact]\n\tpublic async Task PostRestrictionUserNotInRole()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string> { \"blah\" }));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, GetUser());\n\t\tAssert.False(permission.UserCanPost);\n\t\tAssert.NotEmpty(permission.DenialReason);\n\t}\n\n\t[Fact]\n\tpublic async Task ModerateNoUser()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, null);\n\t\tAssert.False(permission.UserCanModerate);\n\t}\n\n\t[Fact]\n\tpublic async Task ModerateUserIsAdmin()\n\t{\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Admin);\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user);\n\t\tAssert.True(permission.UserCanModerate);\n\t}\n\n\t[Fact]\n\tpublic async Task ModerateUserIsModerator()\n\t{\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar permission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user);\n\t\tAssert.True(permission.UserCanModerate);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicClosed()\n\t{\n\t\tvar user = GetUser();\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar premission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user, new Topic { TopicID = 4, IsClosed = true });\n\t\tAssert.False(premission.UserCanPost);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicOpen()\n\t{\n\t\tvar user = GetUser();\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar premission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user, new Topic { TopicID = 4, IsClosed = false });\n\t\tAssert.True(premission.UserCanPost);\n\t}\n\n\t[Fact]\n\tpublic async Task WithUserTopicDeleted()\n\t{\n\t\tvar user = GetUser();\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar premission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user, new Topic { TopicID = 4, IsDeleted = true });\n\t\tAssert.False(premission.UserCanView);\n\t}\n\n\t[Fact]\n\tpublic async Task AnonTopicDeleted()\n\t{\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar premission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, null, new Topic { TopicID = 4, IsDeleted = true });\n\t\tAssert.False(premission.UserCanView);\n\t}\n\n\t[Fact]\n\tpublic async Task ModOnTopicDeleted()\n\t{\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar premission = await forumService.GetPermissionContext(new Forum { ForumID = 1 }, user, new Topic { TopicID = 4, IsDeleted = true });\n\t\tAssert.True(premission.UserCanView);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumNotArchived()\n\t{\n\t\tvar user = GetUser();\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar premission = await forumService.GetPermissionContext(new Forum { ForumID = 1, IsArchived = false }, user, new Topic { TopicID = 4 });\n\t\tAssert.True(premission.UserCanPost);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumIsArchived()\n\t{\n\t\tvar user = GetUser();\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetForumPostRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetForumViewRoles(1).Returns(Task.FromResult(new List<string>()));\n\t\tvar premission = await forumService.GetPermissionContext(new Forum { ForumID = 1, IsArchived = true }, user, new Topic { TopicID = 4 });\n\t\tAssert.False(premission.UserCanPost);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/ForumServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class ForumServiceTests\n{\n\tprivate IForumRepository _mockForumRepo;\n\tprivate ITopicRepository _mockTopicRepo;\n\tprivate ICategoryRepository _mockCategoryRepo;\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate ILastReadService _mockLastReadService;\n\n\tprivate ForumService GetService()\n\t{\n\t\t_mockCategoryRepo = Substitute.For<ICategoryRepository>();\n\t\t_mockForumRepo = Substitute.For<IForumRepository>();\n\t\t_mockTopicRepo = Substitute.For<ITopicRepository>();\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_mockLastReadService = Substitute.For<ILastReadService>();\n\t\treturn new ForumService(_mockForumRepo, _mockTopicRepo, _mockCategoryRepo, _mockSettingsManager, _mockLastReadService);\n\t}\n\n\t[Fact]\n\tpublic async Task Get()\n\t{\n\t\tconst int forumID = 123;\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.Get(forumID).Returns(Task.FromResult(new Forum {ForumID = forumID}));\n\t\tvar forum = await forumService.Get(forumID);\n\t\tAssert.Equal(forumID, forum.ForumID);\n\t\tawait _mockForumRepo.Received().Get(forumID);\n\t}\n\n\t[Fact]\n\tpublic async Task Create()\n\t{\n\t\tvar forumService = GetService();\n\t\tconst int categoryID = 456;\n\t\tconst string title = \"forum title\";\n\t\tconst string desc = \"description of forum\";\n\t\tconst bool isVisible = true;\n\t\tconst bool isArchived = true;\n\t\tconst int sortOrder = 5;\n\t\tconst int forumID = 123;\n\t\tconst string adapter = \"Jeff.Adapter\";\n\t\tconst bool isQAForum = true;\n\t\tvar forum = new Forum {ForumID = forumID, CategoryID = categoryID, Title = title, Description = desc, IsVisible = isVisible, IsArchived = isArchived, SortOrder = sortOrder};\n\t\t_mockForumRepo.Create(categoryID, title, desc, isVisible, isArchived, sortOrder, Arg.Any<String>(), adapter, isQAForum).Returns(Task.FromResult(forum));\n\t\t_mockForumRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\t_mockForumRepo.GetAll().Returns(new List<Forum> { new Forum { ForumID = 1, SortOrder = 9 }, new Forum { ForumID = 2, SortOrder = 6 }, forum});\n\t\tvar result = await forumService.Create(categoryID, title, desc, isVisible, isArchived, sortOrder, adapter, isQAForum);\n\t\tAssert.Equal(forum, result);\n\t\tawait _mockForumRepo.Received().Create(categoryID, title, desc, isVisible, isArchived, sortOrder, Arg.Any<String>(), adapter, isQAForum);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(123, 0);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(2, 2);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(1, 4);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateMakesUrlTitle()\n\t{\n\t\tvar forumService = GetService();\n\t\tconst int categoryID = 456;\n\t\tconst string title = \"forum title\";\n\t\tconst string desc = \"description of forum\";\n\t\tconst bool isVisible = true;\n\t\tconst bool isArchived = true;\n\t\tconst int sortOrder = 5;\n\t\tconst int forumID = 123;\n\t\tconst string adapter = \"Jeff.Adapter\";\n\t\tconst bool isQAForum = true;\n\t\tvar forum = new Forum { ForumID = forumID, CategoryID = categoryID, Title = title, Description = desc, IsVisible = isVisible, IsArchived = isArchived, SortOrder = sortOrder };\n\t\t_mockForumRepo.Create(categoryID, title, desc, isVisible, isArchived, sortOrder, Arg.Any<String>(), adapter, isQAForum).Returns(Task.FromResult(forum));\n\t\t_mockForumRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\tawait forumService.Create(categoryID, title, desc, isVisible, isArchived, sortOrder, adapter, isQAForum);\n\t\tawait _mockForumRepo.Received().Create(categoryID, title, desc, isVisible, isArchived, sortOrder, \"forum-title\", adapter, isQAForum);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateMakesUrlTitleWithAppendage()\n\t{\n\t\tvar forumService = GetService();\n\t\tconst int categoryID = 456;\n\t\tconst string title = \"forum title\";\n\t\tconst string desc = \"description of forum\";\n\t\tconst bool isVisible = true;\n\t\tconst bool isArchived = true;\n\t\tconst int sortOrder = 5;\n\t\tconst int forumID = 123;\n\t\tconst string adapter = \"Jeff.Adapter\";\n\t\tconst bool isQAForum = true;\n\t\tvar forum = new Forum { ForumID = forumID, CategoryID = categoryID, Title = title, Description = desc, IsVisible = isVisible, IsArchived = isArchived, SortOrder = sortOrder };\n\t\t_mockForumRepo.Create(categoryID, title, desc, isVisible, isArchived, sortOrder, Arg.Any<String>(), adapter, isQAForum).Returns(Task.FromResult(forum));\n\t\t_mockForumRepo.GetUrlNamesThatStartWith(title.ToUrlName()).Returns(Task.FromResult(new List<string> {\"forum-title\", \"forum-title-but-not\", \"forum-title-2\"}));\n\t\tawait forumService.Create(categoryID, title, desc, isVisible, isArchived, sortOrder, adapter, isQAForum);\n\t\tawait _mockForumRepo.Received().Create(categoryID, title, desc, isVisible, isArchived, sortOrder, \"forum-title-3\", adapter, isQAForum);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateLast()\n\t{\n\t\tconst int forumID = 123;\n\t\tconst int topicID = 456;\n\t\tvar lastTime = new DateTime(2001, 2, 2);\n\t\tconst string lastName = \"Jeff\";\n\t\tvar forum = new Forum { ForumID = forumID };\n\t\tvar topic = new Topic { TopicID = topicID, LastPostTime = lastTime, LastPostName = lastName };\n\t\tvar forumService = GetService();\n\t\t_mockTopicRepo.GetLastUpdatedTopic(forum.ForumID).Returns(Task.FromResult(topic));\n\t\tawait forumService.UpdateLast(forum);\n\t\tawait _mockTopicRepo.Received().GetLastUpdatedTopic(forum.ForumID);\n\t\tawait _mockForumRepo.Received().UpdateLastTimeAndUser(forum.ForumID, lastTime, lastName);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateLastWithValues()\n\t{\n\t\tvar forumService = GetService();\n\t\tconst int forumID = 123;\n\t\tvar lastTime = new DateTime(2001, 2, 2);\n\t\tconst string lastName = \"Jeff\";\n\t\tvar forum = new Forum { ForumID = forumID };\n\t\tawait forumService.UpdateLast(forum, lastTime, lastName);\n\t\tawait _mockForumRepo.Received().UpdateLastTimeAndUser(forum.ForumID, lastTime, lastName);\n\t}\n\n\t//[Fact]\n\t//[Ignore] // TODO: gotta account for spawned thread\n\t//public void UpdateCounts()\n\t//{\n\t//    const int topicCount = 456;\n\t//    const int postCount = 789;\n\t//    const int forumID = 123;\n\t//    var forum = new Forum(forumID);\n\t//    var forumService = GetService();\n\t//    _mockTopicRepo.GetPostCount(forumID, false).Returns(postCount);\n\t//    _mockTopicRepo.GetTopicCount(forumID, false).Returns(topicCount);\n\t//    forumService.UpdateCounts(forum);\n\t//    _mockTopicRepo.Received().GetPostCount(forumID, false);\n\t//    _mockTopicRepo.Received().GetTopicCount(forumID, false);\n\t//    _mockForumRepo.Verify(f => f.UpdateTopicAndPostCounts(forumID, topicCount, postCount));\n\t//}\n\n\t[Fact]\n\tpublic async Task GetForumsWithCategories()\n\t{\n\t\tvar forums = new List<Forum>();\n\t\tvar cats = new List<Category>();\n\t\tvar forumService = GetService();\n\t\t_mockForumRepo.GetAll().Returns(forums);\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(cats));\n\t\t_mockSettingsManager.Current.ForumTitle.Returns(\"whatever\");\n\t\tvar container = await forumService.GetCategorizedForumContainer();\n\t\tawait _mockCategoryRepo.Received().GetAll();\n\t\tawait _mockForumRepo.Received().GetAll();\n\t\tAssert.Equal(container.AllForums, forums);\n\t\tAssert.Equal(container.AllCategories, cats);\n\t}\n\n\t[Fact]\n\tpublic async Task MoveUp()\n\t{\n\t\tvar f1 = new Forum { ForumID = 123, SortOrder = 0, CategoryID = 777 };\n\t\tvar f2 = new Forum { ForumID = 456, SortOrder = 2, CategoryID = 777 };\n\t\tvar f3 = new Forum { ForumID = 789, SortOrder = 4, CategoryID = 777 };\n\t\tvar f4 = new Forum { ForumID = 1000,SortOrder = 6, CategoryID = 777 };\n\t\tvar forums = new List<Forum> { f1, f2, f3, f4 };\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumsInCategory(777).Returns(Task.FromResult(forums));\n\t\t_mockForumRepo.Get(f3.ForumID).Returns(Task.FromResult(f3));\n\t\tawait service.MoveForumUp(f3.ForumID);\n\t\tawait _mockForumRepo.Received().GetForumsInCategory(777);\n\t\tawait _mockForumRepo.Received(4).UpdateSortOrder(Arg.Any<int>(), Arg.Any<int>());\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(f1.ForumID, f1.SortOrder);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(f2.ForumID, f2.SortOrder);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(f3.ForumID, f3.SortOrder);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(f4.ForumID, f4.SortOrder);\n\t\tAssert.Equal(0, f1.SortOrder);\n\t\tAssert.Equal(2, f3.SortOrder);\n\t\tAssert.Equal(4, f2.SortOrder);\n\t\tAssert.Equal(6, f4.SortOrder);\n\t}\n\n\t[Fact]\n\tpublic async Task MoveDown()\n\t{\n\t\tvar f1 = new Forum { ForumID = 123, SortOrder = 0, CategoryID = 777 };\n\t\tvar f2 = new Forum { ForumID = 456, SortOrder = 2, CategoryID = 777 };\n\t\tvar f3 = new Forum { ForumID = 789, SortOrder = 4, CategoryID = 777 };\n\t\tvar f4 = new Forum { ForumID = 1000, SortOrder = 6, CategoryID = 777 };\n\t\tvar forums = new List<Forum> { f1, f2, f3, f4 };\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumsInCategory(777).Returns(Task.FromResult(forums));\n\t\t_mockForumRepo.Get(f3.ForumID).Returns(Task.FromResult(f3));\n\t\tawait service.MoveForumDown(f3.ForumID);\n\t\tawait _mockForumRepo.Received().GetForumsInCategory(777);\n\t\tawait _mockForumRepo.Received(4).UpdateSortOrder(Arg.Any<int>(), Arg.Any<int>());\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(f1.ForumID, f1.SortOrder);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(f2.ForumID, f2.SortOrder);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(f3.ForumID, f3.SortOrder);\n\t\tawait _mockForumRepo.Received().UpdateSortOrder(f4.ForumID, f4.SortOrder);\n\t\tAssert.Equal(0, f1.SortOrder);\n\t\tAssert.Equal(2, f2.SortOrder);\n\t\tAssert.Equal(4, f4.SortOrder);\n\t\tAssert.Equal(6, f3.SortOrder);\n\t}\n\n\t[Fact]\n\tpublic async Task MoveForumUpThrowsIfNoForum()\n\t{\n\t\tvar service = GetService();\n\t\t_mockForumRepo.Get(Arg.Any<int>()).Returns((Forum) null);\n\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.MoveForumUp(1));\n\t}\n\n\t[Fact]\n\tpublic async Task MoveForumDownThrowsIfNoForum()\n\t{\n\t\tvar service = GetService();\n\t\t_mockForumRepo.Get(Arg.Any<int>()).Returns((Forum)null);\n\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.MoveForumDown(1));\n\t}\n\n\t[Fact]\n\tpublic async Task PostRestrictions()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 1 };\n\t\tvar roles = new List<string> {\"leader\", \"follower\"};\n\t\t_mockForumRepo.GetForumPostRoles(forum.ForumID).Returns(Task.FromResult(roles));\n\t\tvar result = await service.GetForumPostRoles(forum);\n\t\tawait _mockForumRepo.Received().GetForumPostRoles(forum.ForumID);\n\t\tAssert.Same(roles, result);\n\t}\n\n\t[Fact]\n\tpublic async Task ViewRestrictions()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 1 };\n\t\tvar roles = new List<string> { \"leader\", \"follower\" };\n\t\t_mockForumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(roles));\n\t\tvar result = await service.GetForumViewRoles(forum);\n\t\tawait _mockForumRepo.Received().GetForumViewRoles(forum.ForumID);\n\t\tAssert.Same(roles, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetViewableForumIDsFromViewRestrictedForumsReturnsEmptyDictionaryWithoutUser()\n\t{\n\t\tvar graph = new Dictionary<int, List<string>>\n\t\t{\n\t\t\t{1, new List<string> {\"blah\"}},\n\t\t\t{2, new List<string>()},\n\t\t\t{3, new List<string> {\"blah\"}}\n\t\t};\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetAllVisible().Returns(new List<Forum>\n\t\t{\n\t\t\tnew Forum { ForumID = 1 }, new Forum { ForumID = 2 }, new Forum { ForumID = 3 }\n\t\t});\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(graph));\n\t\tvar result = await service.GetViewableForumIDsFromViewRestrictedForums(null);\n\t\tAssert.Single(result);\n\t\tAssert.Equal(2, result[0]);\n\t}\n\n\t[Fact]\n\tpublic async Task GetViewableForumIDsFromViewRestrictedForumsDoesntIncludeForumsWithNoViewRestrictions()\n\t{\n\t\tvar graph = new Dictionary<int, List<string>>();\n\t\tgraph.Add(1, new List<string> { \"blah\" });\n\t\tgraph.Add(2, new List<string>());\n\t\tgraph.Add(3, new List<string> { \"blah\" });\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(graph));\n\t\t_mockForumRepo.GetAllVisible().Returns(new List<Forum>\n\t\t{\n\t\t\tnew Forum { ForumID = 1 }, new Forum { ForumID = 2 }, new Forum { ForumID = 3 }\n\t\t});\n\t\tvar result = await service.GetViewableForumIDsFromViewRestrictedForums(new User { UserID = 123, Roles = new [] {\"blah\"}.ToList() });\n\t\tAssert.Equal(3, result.Count);\n\t}\n\n\t[Fact]\n\tpublic async Task GetViewableForumIDsFromViewRestrictedForumsReturnsIDsWithMatchingUserRoles()\n\t{\n\t\tvar graph = new Dictionary<int, List<string>>();\n\t\tgraph.Add(1, new List<string> { \"blah\" });\n\t\tgraph.Add(2, new List<string>());\n\t\tgraph.Add(3, new List<string> { \"blep\" });\n\t\tgraph.Add(4, new List<string> { \"burp\", \"blah\" });\n\t\tgraph.Add(5, new List<string> { \"burp\" });\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(graph));\n\t\t_mockForumRepo.GetAllVisible().Returns(new List<Forum>\n\t\t{\n\t\t\tnew Forum { ForumID = 1 }, new Forum { ForumID = 2 }, new Forum { ForumID = 3 }, new Forum { ForumID = 4 }, new Forum { ForumID = 5 }\n\t\t});\n\t\tvar result = await service.GetViewableForumIDsFromViewRestrictedForums(new User { UserID = 123, Roles = new[] { \"blah\", \"blep\" }.ToList() });\n\t\tAssert.Equal(4, result.Count);\n\t\tAssert.Contains(1, result);\n\t\tAssert.Contains(2, result);\n\t\tAssert.Contains(3, result);\n\t\tAssert.Contains(4, result);\n\t\tAssert.DoesNotContain(5, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetNonViewableDoesntIncludeForumsWithNoViewRestrictions()\n\t{\n\t\tvar graph = new Dictionary<int, List<string>>();\n\t\tgraph.Add(1, new List<string> { \"blah\" });\n\t\tgraph.Add(2, new List<string>());\n\t\tgraph.Add(3, new List<string> { \"blah\" });\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(graph));\n\t\tvar result = await service.GetNonViewableForumIDs(new User { UserID = 123, Roles = new List<string>()});\n\t\tAssert.Equal(2, result.Count);\n\t\tAssert.DoesNotContain(2, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetNonViewableDoesntIncludeForumsWithRoleMatchingViewRestrictions()\n\t{\n\t\tvar graph = new Dictionary<int, List<string>>();\n\t\tgraph.Add(1, new List<string> { \"blah\" });\n\t\tgraph.Add(2, new List<string>());\n\t\tgraph.Add(3, new List<string> { \"OK\" });\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(graph));\n\t\tvar result = await service.GetNonViewableForumIDs(new User { UserID = 123, Roles = new List<string> { \"OK\" } });\n\t\tAssert.Single(result);\n\t\tAssert.DoesNotContain(3, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetNonViewableIncludesForumsWithNoMatchingViewRestrictions()\n\t{\n\t\tvar graph = new Dictionary<int, List<string>>();\n\t\tgraph.Add(1, new List<string> { \"blah\" });\n\t\tgraph.Add(2, new List<string>());\n\t\tgraph.Add(3, new List<string> { \"OK\" });\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(graph));\n\t\tvar result = await service.GetNonViewableForumIDs(new User { UserID = 123, Roles = new List<string> { \"OK\" } });\n\t\tAssert.Single(result);\n\t\tAssert.Equal(1, result[0]);\n\t}\n\n\t[Fact]\n\tpublic async Task GetNonViewableExcludesViewRestrictionsForNoUser()\n\t{\n\t\tvar graph = new Dictionary<int, List<string>>();\n\t\tgraph.Add(1, new List<string> { \"blah\" });\n\t\tgraph.Add(2, new List<string>());\n\t\tgraph.Add(3, new List<string> { \"OK\" });\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(graph));\n\t\tvar result = await service.GetNonViewableForumIDs(null);\n\t\tAssert.Equal(2, result.Count);\n\t\tAssert.Equal(1, result[0]);\n\t\tAssert.Equal(3, result[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task GetCategorizedForUserHasOnlyViewableForums()\n\t{\n\t\tvar graph = new Dictionary<int, List<string>>();\n\t\tgraph.Add(1, new List<string> { \"blah\" });\n\t\tgraph.Add(2, new List<string>());\n\t\tgraph.Add(3, new List<string> { \"OK\" });\n\t\tvar allForums = new List<Forum> {new Forum { ForumID = 1 }, new Forum { ForumID = 2 }, new Forum { ForumID = 3 } };\n\t\tvar service = GetService();\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(graph));\n\t\t_mockForumRepo.GetAllVisible().Returns(allForums);\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(new List<Category>()));\n\t\t_mockSettingsManager.Current.ForumTitle.Returns(\"whatever\");\n\t\tvar container = await service.GetCategorizedForumContainerFilteredForUser(new User { UserID = 123, Roles = new List<string> { \"OK\" } });\n\t\tAssert.Equal(2, container.UncategorizedForums.Count);\n\t\tAssert.Null(container.UncategorizedForums.SingleOrDefault(f => f.ForumID == 1));\n\t}\n\n\t[Fact]\n\tpublic async Task GetCategorizedForUserPopulatesReadStatus()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(new List<Category>()));\n\t\t_mockForumRepo.GetAllVisible().Returns(new List<Forum>());\n\t\t_mockForumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(new Dictionary<int, List<string>>()));\n\t\t_mockSettingsManager.Current.ForumTitle.Returns(\"\");\n\t\tawait service.GetCategorizedForumContainerFilteredForUser(user);\n\t\tawait _mockLastReadService.Received(1).GetForumReadStatus(user, Arg.Any<CategorizedForumContainer>());\n\t}\n\n\t[Fact]\n\tpublic async Task GetCategoryContainersWithForumsMapsCatsWithUnCatForums()\n\t{\n\t\tvar service = GetService();\n\t\tvar categories = new List<Category>\n\t\t{\n\t\t\tnew Category {CategoryID = 1, SortOrder = 5},\n\t\t\tnew Category {CategoryID = 2, SortOrder = 1},\n\t\t\tnew Category {CategoryID = 3, SortOrder = 3}\n\t\t};\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(categories));\n\t\tvar forums = new List<Forum>\n\t\t{\n\t\t\tnew Forum {ForumID = 1, CategoryID = null},\n\t\t\tnew Forum {ForumID = 2, CategoryID = categories[0].CategoryID, SortOrder = 3},\n\t\t\tnew Forum {ForumID = 3, CategoryID = categories[0].CategoryID, SortOrder = 1},\n\t\t\tnew Forum {ForumID = 4, CategoryID = categories[0].CategoryID, SortOrder = 7},\n\t\t\tnew Forum {ForumID = 5, CategoryID = categories[0].CategoryID, SortOrder = 5},\n\t\t\tnew Forum {ForumID = 6, CategoryID = categories[2].CategoryID}\n\t\t};\n\t\t_mockForumRepo.GetAll().Returns(forums);\n\n\t\tvar result = await service.GetCategoryContainersWithForums();\n\n\t\tAssert.Equal(0, result[0].Category.CategoryID);\n\t\tAssert.Equal(2, result[1].Category.CategoryID);\n\t\tAssert.Equal(3, result[2].Category.CategoryID);\n\t\tAssert.Equal(1, result[3].Category.CategoryID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetCategoryContainersWithForumsMapsCatsWithoutUnCatForums()\n\t{\n\t\tvar service = GetService();\n\t\tvar categories = new List<Category>\n\t\t{\n\t\t\tnew Category {CategoryID = 1, SortOrder = 5},\n\t\t\tnew Category {CategoryID = 2, SortOrder = 1},\n\t\t\tnew Category {CategoryID = 3, SortOrder = 3}\n\t\t};\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(categories));\n\t\tvar forums = new List<Forum>\n\t\t{\n\t\t\tnew Forum {ForumID = 2, CategoryID = categories[0].CategoryID, SortOrder = 3},\n\t\t\tnew Forum {ForumID = 3, CategoryID = categories[0].CategoryID, SortOrder = 1},\n\t\t\tnew Forum {ForumID = 4, CategoryID = categories[0].CategoryID, SortOrder = 7},\n\t\t\tnew Forum {ForumID = 5, CategoryID = categories[0].CategoryID, SortOrder = 5},\n\t\t\tnew Forum {ForumID = 6, CategoryID = categories[2].CategoryID}\n\t\t};\n\t\t_mockForumRepo.GetAll().Returns(forums);\n\n\t\tvar result = await service.GetCategoryContainersWithForums();\n\n\t\tAssert.Equal(2, result[0].Category.CategoryID);\n\t\tAssert.Equal(3, result[1].Category.CategoryID);\n\t\tAssert.Equal(1, result[2].Category.CategoryID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetCategoryContainersWithForumsMapsForums()\n\t{\n\t\tvar service = GetService();\n\t\tvar categories = new List<Category>\n\t\t{\n\t\t\tnew Category {CategoryID = 1, SortOrder = 5},\n\t\t\tnew Category {CategoryID = 2, SortOrder = 1},\n\t\t\tnew Category {CategoryID = 3, SortOrder = 3}\n\t\t};\n\t\t_mockCategoryRepo.GetAll().Returns(Task.FromResult(categories));\n\t\tvar forums = new List<Forum>\n\t\t{\n\t\t\tnew Forum {ForumID = 1, CategoryID = null, SortOrder = 3},\n\t\t\tnew Forum {ForumID = 2, CategoryID = categories[0].CategoryID, SortOrder = 3},\n\t\t\tnew Forum {ForumID = 3, CategoryID = categories[0].CategoryID, SortOrder = 1},\n\t\t\tnew Forum {ForumID = 4, CategoryID = categories[0].CategoryID, SortOrder = 7},\n\t\t\tnew Forum {ForumID = 5, CategoryID = categories[0].CategoryID, SortOrder = 5},\n\t\t\tnew Forum {ForumID = 6, CategoryID = categories[2].CategoryID},\n\t\t\tnew Forum {ForumID = 7, CategoryID = null, SortOrder = 1},\n\t\t};\n\t\t_mockForumRepo.GetAll().Returns(forums);\n\n\t\tvar result = await service.GetCategoryContainersWithForums();\n\n\t\tAssert.Equal(7, result[0].Forums.ToArray()[0].ForumID);\n\t\tAssert.Equal(1, result[0].Forums.ToArray()[1].ForumID);\n\t\tAssert.Equal(3, result[3].Forums.ToArray()[0].ForumID);\n\t\tAssert.Equal(2, result[3].Forums.ToArray()[1].ForumID);\n\t\tAssert.Equal(5, result[3].Forums.ToArray()[2].ForumID);\n\t\tAssert.Equal(4, result[3].Forums.ToArray()[3].ForumID);\n\t\tAssert.Equal(6, result[2].Forums.ToArray()[0].ForumID);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerForQAMapsBaseProperties()\n\t{\n\t\tvar topicContainer = new TopicContainer\n\t\t{\n\t\t\tForum = new Forum { ForumID = 1 },\n\t\t\tTopic = new Topic { TopicID = 2 },\n\t\t\tPosts = new List<Post> {new Post { PostID = 123, IsFirstInTopic = true }},\n\t\t\tPagerContext = new PagerContext(),\n\t\t\tPermissionContext = new ForumPermissionContext(),\n\t\t\tSignatures = new Dictionary<int, string>(),\n\t\t\tAvatars = new Dictionary<int, int>(),\n\t\t\tVotedPostIDs = new List<int>(),\n\t\t\tTopicState = new TopicState()\n\t\t};\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.Same(topicContainer.Forum, result.Forum);\n\t\tAssert.Same(topicContainer.Topic, result.Topic);\n\t\tAssert.Same(topicContainer.Posts, result.Posts);\n\t\tAssert.Same(topicContainer.PagerContext, result.PagerContext);\n\t\tAssert.Same(topicContainer.PermissionContext, result.PermissionContext);\n\t\tAssert.Same(topicContainer.Signatures, result.Signatures);\n\t\tAssert.Same(topicContainer.Avatars, result.Avatars);\n\t\tAssert.Same(topicContainer.VotedPostIDs, result.VotedPostIDs);\n\t\tAssert.Same(topicContainer.TopicState, result.TopicState);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerGrabsFirstPostForQuestion()\n\t{\n\t\tvar posts = new List<Post>\n\t\t{\n\t\t\tnew Post {PostID = 1},\n\t\t\tnew Post{PostID = 2, IsFirstInTopic = true}\n\t\t};\n\t\tvar topicContainer = new TopicContainer {Posts = posts, Topic = new Topic { TopicID = 123 }};\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.Equal(2, result.QuestionPostWithComments.Post.PostID);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerThrowsWithNoFirstInTopicPost()\n\t{\n\t\tvar posts = new List<Post>\n\t\t{\n\t\t\tnew Post { PostID =  1 },\n\t\t\tnew Post { PostID =  2 }\n\t\t};\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = new Topic { TopicID = 123 } };\n\t\tvar service = GetService();\n\t\tAssert.Throws<InvalidOperationException>(() => service.MapTopicContainerForQA(topicContainer));\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerThrowsWithMoreThanOneFirstInTopicPost()\n\t{\n\t\tvar posts = new List<Post>\n\t\t{\n\t\t\tnew Post { PostID =  1, IsFirstInTopic = true},\n\t\t\tnew Post { PostID =  2, IsFirstInTopic = true}\n\t\t};\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = new Topic { TopicID = 123 } };\n\t\tvar service = GetService();\n\t\tAssert.Throws<InvalidOperationException>(() => service.MapTopicContainerForQA(topicContainer));\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerSetsQuestionsWithNoParentAsAnswers()\n\t{\n\t\tvar post1 = new Post { PostID = 1, ParentPostID = 0};\n\t\tvar post2 = new Post { PostID = 2, IsFirstInTopic = true};\n\t\tvar post3 = new Post { PostID = 3, ParentPostID = 2};\n\t\tvar post4 = new Post { PostID = 4, ParentPostID = 1};\n\t\tvar post5 = new Post { PostID = 5, ParentPostID = 3};\n\t\tvar posts = new List<Post> {post1, post2, post3, post4, post5};\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = new Topic { TopicID = 1234 } };\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.Single(result.AnswersWithComments);\n\t\tAssert.Same(post1, result.AnswersWithComments[0].Post);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerMapsCommentsToParentQuestionsAndAnswers()\n\t{\n\t\tvar post1 = new Post { PostID = 1, ParentPostID = 0 };\n\t\tvar post2 = new Post { PostID = 2, IsFirstInTopic = true };\n\t\tvar post3 = new Post { PostID = 3, ParentPostID = 0 };\n\t\tvar post4 = new Post { PostID = 4, ParentPostID = 1 };\n\t\tvar post5 = new Post { PostID = 5, ParentPostID = 2 };\n\t\tvar post6 = new Post { PostID = 6, ParentPostID = 3 };\n\t\tvar post7 = new Post { PostID = 7, ParentPostID = 3 };\n\t\tvar posts = new List<Post> { post1, post2, post3, post4, post5, post6, post7 };\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = new Topic { TopicID = 1234 } };\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.True(result.AnswersWithComments[0].Children.Count == 1);\n\t\tAssert.Contains(post4, result.AnswersWithComments[0].Children);\n\t\tAssert.True(result.AnswersWithComments[1].Children.Count == 2);\n\t\tAssert.Contains(post6, result.AnswersWithComments[1].Children);\n\t\tAssert.Contains(post7, result.AnswersWithComments[1].Children);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerMapsCommentsToQuestion()\n\t{\n\t\tvar post1 = new Post { PostID = 1, ParentPostID = 0 };\n\t\tvar post2 = new Post { PostID = 2, IsFirstInTopic = true };\n\t\tvar post3 = new Post { PostID = 3, ParentPostID = 0 };\n\t\tvar post4 = new Post { PostID = 4, ParentPostID = 1 };\n\t\tvar post5 = new Post { PostID = 5, ParentPostID = 2 };\n\t\tvar post6 = new Post { PostID = 6, ParentPostID = 2 };\n\t\tvar post7 = new Post { PostID = 7, ParentPostID = 3 };\n\t\tvar posts = new List<Post> { post1, post2, post3, post4, post5, post6, post7 };\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = new Topic { TopicID = 1234 } };\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.True(result.QuestionPostWithComments.Children.Count == 2);\n\t\tAssert.Contains(post5, result.QuestionPostWithComments.Children);\n\t\tAssert.Contains(post6, result.QuestionPostWithComments.Children);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerOrdersAnswersByVoteThenDate()\n\t{\n\t\tvar post1 = new Post { PostID = 1, IsFirstInTopic = true };\n\t\tvar post2 = new Post { PostID = 2, Votes = 7, PostTime = new DateTime(2000, 1, 1) };\n\t\tvar post3 = new Post { PostID = 3, Votes = 7, PostTime = new DateTime(2000, 2, 1) };\n\t\tvar post4 = new Post { PostID = 4, Votes = 2 };\n\t\tvar post5 = new Post { PostID = 5, Votes = 3 };\n\t\tvar post6 = new Post { PostID = 6, Votes = 8 };\n\t\tvar post7 = new Post { PostID = 7, Votes = 5 };\n\t\tvar posts = new List<Post> { post1, post2, post3, post4, post5, post6, post7 };\n\t\tvar topic = new Topic { TopicID = 123, AnswerPostID = null };\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = topic };\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.Same(post6, result.AnswersWithComments[0].Post);\n\t\tAssert.Same(post3, result.AnswersWithComments[1].Post);\n\t\tAssert.Same(post2, result.AnswersWithComments[2].Post);\n\t\tAssert.Same(post7, result.AnswersWithComments[3].Post);\n\t\tAssert.Same(post5, result.AnswersWithComments[4].Post);\n\t\tAssert.Same(post4, result.AnswersWithComments[5].Post);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerOrdersAnswersByAnswerThenVoteThenDate()\n\t{\n\t\tvar post1 = new Post { PostID = 1, IsFirstInTopic = true };\n\t\tvar post2 = new Post { PostID = 2, Votes = 7, PostTime = new DateTime(2000, 1, 1) };\n\t\tvar post3 = new Post { PostID = 3, Votes = 7, PostTime = new DateTime(2000, 2, 1) };\n\t\tvar post4 = new Post { PostID = 4, Votes = 2 };\n\t\tvar post5 = new Post { PostID = 5, Votes = 3 };\n\t\tvar post6 = new Post { PostID = 6, Votes = 8 };\n\t\tvar post7 = new Post { PostID = 7, Votes = 5 };\n\t\tvar posts = new List<Post> { post1, post2, post3, post4, post5, post6, post7 };\n\t\tvar topic = new Topic { TopicID = 123, AnswerPostID = 5};\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = topic };\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.Same(post5, result.AnswersWithComments[0].Post);\n\t\tAssert.Same(post6, result.AnswersWithComments[1].Post);\n\t\tAssert.Same(post3, result.AnswersWithComments[2].Post);\n\t\tAssert.Same(post2, result.AnswersWithComments[3].Post);\n\t\tAssert.Same(post7, result.AnswersWithComments[4].Post);\n\t\tAssert.Same(post4, result.AnswersWithComments[5].Post);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerDoesNotMapCommentsForTopQuestionAsReplies()\n\t{\n\t\tvar post1 = new Post { PostID = 1, ParentPostID = 0 };\n\t\tvar post2 = new Post { PostID = 2, IsFirstInTopic = true };\n\t\tvar post3 = new Post { PostID = 3, ParentPostID = 0 };\n\t\tvar post4 = new Post { PostID = 4, ParentPostID = 1 };\n\t\tvar post5 = new Post { PostID = 5, ParentPostID = 2 };\n\t\tvar post6 = new Post { PostID = 6, ParentPostID = 3 };\n\t\tvar post7 = new Post { PostID = 7, ParentPostID = 3 };\n\t\tvar posts = new List<Post> { post1, post2, post3, post4, post5, post6, post7 };\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = new Topic { TopicID = 1234 } };\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.DoesNotContain(result.AnswersWithComments, x => x.Post.PostID == post5.PostID);\n\t}\n\n\t[Fact]\n\tpublic void MapTopicContainerMapsLastReadTimeToQuestionAndAnswerSets()\n\t{\n\t\tvar post1 = new Post { PostID = 1, ParentPostID = 0 };\n\t\tvar post2 = new Post { PostID = 2, IsFirstInTopic = true };\n\t\tvar post3 = new Post { PostID = 3, ParentPostID = 0 };\n\t\tvar post4 = new Post { PostID = 4, ParentPostID = 1 };\n\t\tvar post5 = new Post { PostID = 5, ParentPostID = 2 };\n\t\tvar post6 = new Post { PostID = 6, ParentPostID = 3 };\n\t\tvar post7 = new Post { PostID = 7, ParentPostID = 3 };\n\t\tvar posts = new List<Post> { post1, post2, post3, post4, post5, post6, post7 };\n\t\tvar lastRead = new DateTime(2000, 1, 1);\n\t\tvar topicContainer = new TopicContainer { Posts = posts, Topic = new Topic { TopicID = 1234 }, LastReadTime = lastRead };\n\t\tvar service = GetService();\n\t\tvar result = service.MapTopicContainerForQA(topicContainer);\n\t\tAssert.Equal(lastRead, result.AnswersWithComments[0].LastReadTime);\n\t\tAssert.Equal(lastRead, result.AnswersWithComments[1].LastReadTime);\n\t\tAssert.Equal(lastRead, result.QuestionPostWithComments.LastReadTime);\n\t}\n\n\tpublic class ModifyForumRoles : ForumServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ThrowsIfNoForumMatch()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_mockForumRepo.Get(Arg.Any<int>()).Returns((Forum) null);\n\n\t\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.ModifyForumRoles(new ModifyForumRolesContainer()));\n\t\t}\n\n\t\tprivate async Task<Tuple<int, string>> CallSetup(ModifyForumRolesType modifyType)\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = 123 };\n\t\t\tvar role = \"role\";\n\t\t\t_mockForumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\tawait service.ModifyForumRoles(new ModifyForumRolesContainer { ForumID = forum.ForumID, ModifyType = modifyType, Role = role });\n\t\t\treturn Tuple.Create(forum.ForumID, role);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task AddPostCallsRepo()\n\t\t{\n\t\t\tvar (forumID, role) = await CallSetup(ModifyForumRolesType.AddPost);\n\n\t\t\tawait _mockForumRepo.Received().AddPostRole(forumID, role);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task RemovePostCallsRepo()\n\t\t{\n\t\t\tvar (forumID, role) = await CallSetup(ModifyForumRolesType.RemovePost);\n\n\t\t\tawait _mockForumRepo.Received().RemovePostRole(forumID, role);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task AddViewCallsRepo()\n\t\t{\n\t\t\tvar (forumID, role) = await CallSetup(ModifyForumRolesType.AddView);\n\n\t\t\tawait _mockForumRepo.Received().AddViewRole(forumID, role);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task RemoveViewCallsRepo()\n\t\t{\n\t\t\tvar (forumID, role) = await CallSetup(ModifyForumRolesType.RemoveView);\n\n\t\t\tawait _mockForumRepo.Received().RemoveViewRole(forumID, role);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task RemoveAllPostCallsRepo()\n\t\t{\n\t\t\tvar (forumID, _) = await CallSetup(ModifyForumRolesType.RemoveAllPost);\n\n\t\t\tawait _mockForumRepo.Received().RemoveAllPostRoles(forumID);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task RemoveAllViewCallsRepo()\n\t\t{\n\t\t\tvar (forumID, _) = await CallSetup(ModifyForumRolesType.RemoveAllView);\n\n\t\t\tawait _mockForumRepo.Received().RemoveAllViewRoles(forumID);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/ImageServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class ImageServiceTests\n{\n\tprivate IUserImageRepository _imageRepo;\n\tprivate IUserAvatarRepository _avatarRepo;\n\tprivate IProfileService _profileService;\n\tprivate IUserRepository _userRepo;\n\tprivate ISettingsManager _settingsManager;\n\n\tprivate ImageService GetService()\n\t{\n\t\t_imageRepo = Substitute.For<IUserImageRepository>();\n\t\t_avatarRepo = Substitute.For<IUserAvatarRepository>();\n\t\t_profileService = Substitute.For<IProfileService>();\n\t\t_userRepo = Substitute.For<IUserRepository>();\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\treturn new ImageService(_avatarRepo, _imageRepo, _profileService, _userRepo, _settingsManager);\n\t}\n\n\t[Fact]\n\tpublic async Task GetAvatar()\n\t{\n\t\tvar service = GetService();\n\t\tvar streamResponse = Substitute.For<IStreamResponse>();\n\t\t_avatarRepo.GetImageStream(1).Returns(Task.FromResult(streamResponse));\n\n\t\tvar result = await service.GetAvatarImageStream(1);\n\n\t\tAssert.Same(streamResponse, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetUserImage()\n\t{\n\t\tvar service = GetService();\n\t\tvar streamResponse = Substitute.For<IStreamResponse>();\n\t\t_imageRepo.GetImageStream(1).Returns(Task.FromResult(streamResponse));\n\n\t\tvar result = await service.GetUserImageStream(1);\n\n\t\tAssert.Same(streamResponse, result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/LastReadServiceTests.cs",
    "content": "namespace PopForums.Test.Services;\n\npublic class LastReadServiceTests\n{\n\tprivate LastReadService GetService()\n\t{\n\t\t_lastReadRepo = Substitute.For<ILastReadRepository>();\n\t\t_postRepo = Substitute.For<IPostRepository>();\n\t\treturn new LastReadService(_lastReadRepo, _postRepo);\n\t}\n\n\tprivate ILastReadRepository _lastReadRepo;\n\tprivate IPostRepository _postRepo;\n\n\t[Fact]\n\tpublic async Task MarkForumReadSetsReadTime()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 123 };\n\t\tvar user = new User { UserID = 456 };\n\t\tawait service.MarkForumRead(user, forum);\n\t\tawait _lastReadRepo.Received(1).SetForumRead(user.UserID, forum.ForumID, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task MarkForumReadDeletesOldTopicReadTimes()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 123 };\n\t\tvar user = new User { UserID = 456 };\n\t\tawait service.MarkForumRead(user, forum);\n\t\tawait _lastReadRepo.Received(1).DeleteTopicReadsInForum(user.UserID, forum.ForumID);\n\t}\n\n\t[Fact]\n\tpublic async Task MarkTopicReadThrowsWithoutUser()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(async () => await service.MarkTopicRead(null, new Topic { TopicID = 1 }));\n\t}\n\n\t[Fact]\n\tpublic async Task MarkTopicReadThrowsWithoutTopic()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(async () => await service.MarkTopicRead(new User(), null));\n\t}\n\n\t[Fact]\n\tpublic async Task MarkAllForumReadThrowsWithoutUser()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(async () => await service.MarkAllForumsRead(null));\n\t}\n\n\t[Fact]\n\tpublic async Task MarkForumReadThrowsWithoutUser()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(async () => await service.MarkForumRead(null, new Forum { ForumID = 1 }));\n\t}\n\n\t[Fact]\n\tpublic async Task MarkForumReadThrowsWithoutForum()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(async () => await service.MarkForumRead(new User(), null));\n\t}\n\n\t[Fact]\n\tpublic async Task MarkAllForumReadSetsReadTimes()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 456 };\n\t\tawait service.MarkAllForumsRead(user);\n\t\tawait _lastReadRepo.Received(1).SetAllForumsRead(user.UserID, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task MarkAllForumReadDeletesAllOldTopicReadTimes()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 456 };\n\t\tawait service.MarkAllForumsRead(user);\n\t\tawait _lastReadRepo.Received(1).DeleteAllTopicReads(user.UserID);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumReadStatusForNoUser()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum1 = new Forum { ForumID = 1 };\n\t\tvar forum2 = new Forum { ForumID = 2, IsArchived = true };\n\t\tvar forum3 = new Forum { ForumID = 3 };\n\t\tvar container = new CategorizedForumContainer(new List<Category>(), new[] { forum1, forum2, forum3 });\n\t\tawait service.GetForumReadStatus(null, container);\n\t\tAssert.Equal(3, container.ReadStatusLookup.Count);\n\t\tAssert.Equal(ReadStatus.NoNewPosts, container.ReadStatusLookup[1]);\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Closed, container.ReadStatusLookup[2]);\n\t\tAssert.Equal(ReadStatus.NoNewPosts, container.ReadStatusLookup[3]);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumReadStatusUserNewPosts()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 1, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) };\n\t\tvar user = new User { UserID = 2 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(2).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\tvar container = new CategorizedForumContainer(new List<Category>(), new[] { forum });\n\t\tawait service.GetForumReadStatus(user, container);\n\t\tAssert.Single(container.ReadStatusLookup);\n\t\tAssert.Equal(ReadStatus.NewPosts, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumReadStatusUserNewPostsButNoTopicRecords()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 1, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) };\n\t\tvar user = new User { UserID = 2 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(2).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t_lastReadRepo.GetLastReadTimesForForum(user.UserID, forum.ForumID).Returns(new DateTime(2000, 1, 1, 3, 0, 0));\n\t\tvar container = new CategorizedForumContainer(new List<Category>(), new[] { forum });\n\t\tawait service.GetForumReadStatus(user, container);\n\t\tAssert.Single(container.ReadStatusLookup);\n\t\tAssert.Equal(ReadStatus.NewPosts, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumReadStatusUserNewPostsNoLastReadRecords()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 1, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) };\n\t\tvar user = new User { UserID = 2 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(2).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\tvar container = new CategorizedForumContainer(new List<Category>(), new[] { forum });\n\t\tawait service.GetForumReadStatus(user, container);\n\t\tAssert.Single(container.ReadStatusLookup);\n\t\tAssert.Equal(ReadStatus.NewPosts, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumReadStatusUserNoNewPosts()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 1, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) };\n\t\tvar user = new User { UserID = 2 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(2).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\tvar container = new CategorizedForumContainer(new List<Category>(), new[] { forum });\n\t\tawait service.GetForumReadStatus(user, container);\n\t\tAssert.Single(container.ReadStatusLookup);\n\t\tAssert.Equal(ReadStatus.NoNewPosts, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumReadStatusUserNewPostsArchived()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 1, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0), IsArchived = true };\n\t\tvar user = new User { UserID = 2 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(2).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\tvar container = new CategorizedForumContainer(new List<Category>(), new[] { forum });\n\t\tawait service.GetForumReadStatus(user, container);\n\t\tAssert.Single(container.ReadStatusLookup);\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Closed, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task ForumReadStatusUserNoNewPostsArchived()\n\t{\n\t\tvar service = GetService();\n\t\tvar forum = new Forum { ForumID = 1, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0), IsArchived = true };\n\t\tvar user = new User { UserID = 2 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(2).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\tvar container = new CategorizedForumContainer(new List<Category>(), new[] { forum });\n\t\tawait service.GetForumReadStatus(user, container);\n\t\tAssert.Single(container.ReadStatusLookup);\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Closed, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusForNoUser()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> {new Topic { TopicID = 1 }, new Topic { TopicID = 2, IsClosed = true}, new Topic { TopicID = 3, IsPinned = true}};\n\t\tawait service.GetTopicReadStatus(null, container);\n\t\tAssert.Equal(3, container.ReadStatusLookup.Count);\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Closed | ReadStatus.NotPinned, container.ReadStatusLookup[2]);\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Open | ReadStatus.Pinned, container.ReadStatusLookup[3]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserNewNoForumRecordNoTopicRecord()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserNewNoForumRecordWithTopicRecord()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserNewWithForumRecordNoTopicRecord()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 2, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserNewWithForumRecordWithTopicRecord()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 2, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserNotNewWithForumRecordNoTopicRecord()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserNotNewNoForumRecordWithTopicRecord()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserNotNewWithForumRecordWithTopicRecordForumNewer()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserNotNewWithForumRecordWithTopicRecordTopicNewer()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 1, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserOpenNewPinned()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, IsPinned = true, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Open | ReadStatus.Pinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserOpenNewNotPinned()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserOpenNotNewPinned()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, IsPinned = true, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Open | ReadStatus.Pinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserOpenNotNewNotPinned()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Open | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserClosedNewPinned()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, IsClosed = true, IsPinned = true, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(new Dictionary<int, DateTime>());\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Closed | ReadStatus.Pinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserClosedNewNotPinned()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, IsClosed = true, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 3, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NewPosts | ReadStatus.Closed | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserClosedNoNewPinned()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, IsClosed = true, IsPinned = true, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\t\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\t\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Closed | ReadStatus.Pinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task TopicReadStatusWithUserClosedNoNewNotPinned()\n\t{\n\t\tvar service = GetService();\n\t\tvar container = new PagedTopicContainer();\n\t\tcontainer.Topics = new List<Topic> { new Topic { TopicID = 1, ForumID = 2, IsClosed = true, LastPostTime = new DateTime(2000, 1, 1, 5, 0, 0) } };\n\t\tvar user = new User { UserID = 123 };\n\t\t_lastReadRepo.GetLastReadTimesForForums(user.UserID).Returns(Task.FromResult(new Dictionary<int, DateTime> { { 2, new DateTime(2000, 1, 1, 7, 0, 0) } }));\n\t\t_lastReadRepo.GetLastReadTimesForTopics(user.UserID, Arg.Is<IEnumerable<int>>(x => x.SequenceEqual(new[] { 1 }))).Returns(Task.FromResult(new Dictionary<int, DateTime>()));\n\t\tawait service.GetTopicReadStatus(user, container);\n\t\tAssert.Equal(ReadStatus.NoNewPosts | ReadStatus.Closed | ReadStatus.NotPinned, container.ReadStatusLookup[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task MarkTopicReadCallsRepo()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar topic = new Topic { TopicID = 2 };\n\t\tawait service.MarkTopicRead(user, topic);\n\t\tawait _lastReadRepo.Received(1).SetTopicRead(user.UserID, topic.TopicID, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task GetLastReadTimeReturnsTopicTimeWhenAvailable()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar topic = new Topic { TopicID = 2 };\n\t\tvar lastRead = new DateTime(2010, 1, 1);\n\t\t_lastReadRepo.GetLastReadTimeForTopic(user.UserID, topic.TopicID).Returns(lastRead);\n\n\t\tvar result = await service.GetLastReadTime(user, topic);\n\n\t\tAssert.Equal(lastRead, result);\n\t\tawait _lastReadRepo.DidNotReceive().GetLastReadTimesForForum(Arg.Any<int>(), Arg.Any<int>());\n\t}\n\n\t[Fact]\n\tpublic async Task GetLastReadTimeReturnsForumTimeWhenNoTopicTimeAvailable()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar topic = new Topic { TopicID = 2, ForumID = 3};\n\t\tvar lastRead = new DateTime(2010, 1, 1);\n\t\t_lastReadRepo.GetLastReadTimeForTopic(user.UserID, topic.TopicID).Returns((DateTime?)null);\n\t\t_lastReadRepo.GetLastReadTimesForForum(user.UserID, topic.ForumID).Returns(lastRead);\n\n\t\tvar result = await service.GetLastReadTime(user, topic);\n\n\t\tAssert.Equal(lastRead, result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/PostImageCleanupWorkerTests.cs",
    "content": "using NSubstitute.ExceptionExtensions;\n\nnamespace PopForums.Test.Services;\n\npublic class PostImageCleanupWorkerTests\n{\n\tprivate IPostImageService _postImageService;\n\tprivate IErrorLog _errorLog;\n\n\tprivate PostImageCleanupWorker GetWorker()\n\t{\n\t\t_postImageService = Substitute.For<IPostImageService>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\treturn new PostImageCleanupWorker(_postImageService, _errorLog);\n\t}\n\n\t[Fact]\n\tpublic void NoErrorNoLog()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_postImageService.DeleteOldPostImages().Returns(Task.CompletedTask);\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_errorLog.DidNotReceive().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\n\t[Fact]\n\tpublic void LogWhenThrows()\n\t{\n\t\tvar worker = GetWorker(); \n\t\t_postImageService.DeleteOldPostImages().ThrowsAsync<Exception>();\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_errorLog.Received().Log(Arg.Any<Exception>(),ErrorSeverity.Error);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/PostImageServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class PostImageServiceTests\n{\n\tprivate IImageService _imageService;\n\tprivate IPostImageRepository _postImageRepository;\n\tprivate IPostImageTempRepository _postImageTempRepository;\n\tprivate ISettingsManager _settingsManager;\n\tprivate ITenantService _tenantService;\n\n\tprotected PostImageService GetService()\n\t{\n\t\t_imageService = Substitute.For<IImageService>();\n\t\t_postImageRepository = Substitute.For<IPostImageRepository>();\n\t\t_postImageTempRepository = Substitute.For<IPostImageTempRepository>();\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_tenantService = Substitute.For<ITenantService>();\n\t\treturn new PostImageService(_imageService, _postImageRepository, _postImageTempRepository, _settingsManager, _tenantService);\n\t}\n\n\tpublic class ProcessImageIsOk : PostImageServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic void ImageIsTooBig()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\tvar array = new byte[1025];\n\t\t\tvar contentType = \"blah\";\n\n\t\t\tvar result = service.ProcessImageIsOk(array, contentType);\n\n\t\t\tAssert.False(result);\n\t\t\t_imageService.DidNotReceive().ConstrainResize(Arg.Any<byte[]>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<int>(), false);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void ImageIsBadContentType()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\tvar array = new byte[1024];\n\t\t\tvar contentType = \"blah\";\n\n\t\t\tvar result = service.ProcessImageIsOk(array, contentType);\n\n\t\t\tAssert.False(result);\n\t\t\t_imageService.DidNotReceive().ConstrainResize(Arg.Any<byte[]>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<int>(), false);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void ImageIsRightSizeJpeg()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\tvar array = new byte[1024];\n\t\t\tvar contentType = \"image/jpeg\";\n\n\t\t\tvar result = service.ProcessImageIsOk(array, contentType);\n\n\t\t\tAssert.True(result);\n\t\t}\n\n\n\t\t[Fact]\n\t\tpublic void ImageIsRightSizeGif()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\tvar array = new byte[1024];\n\t\t\tvar contentType = \"image/gif\";\n\n\t\t\tvar result = service.ProcessImageIsOk(array, contentType);\n\n\t\t\tAssert.True(result);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void ImageIsResized()\n\t\t{\n\t\t\tvar height = 100;\n\t\t\tvar width = 200;\n\t\t\tvar service = GetService();\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\t_settingsManager.Current.PostImageMaxHeight.Returns(height);\n\t\t\t_settingsManager.Current.PostImageMaxWidth.Returns(width);\n\t\t\tvar array = new byte[1];\n\t\t\tvar contentType = \"image/jpeg\";\n\n\t\t\tvar result = service.ProcessImageIsOk(array, contentType);\n\n\t\t\tAssert.True(result);\n\t\t\t_imageService.Received().ConstrainResize(array, width, height, 60, false);\n\t\t}\n\t}\n\n\tpublic class PersistAndGetPayload : PostImageServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ThrowsWithNoContentType()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\tservice.ProcessImageIsOk(new byte[1], \"\");\n\n\t\t\tawait Assert.ThrowsAsync<Exception>(() => service.PersistAndGetPayload());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task ThrowsWhenNotOkContentType()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\tservice.ProcessImageIsOk(new byte[1], \"blah\");\n\n\t\t\tawait Assert.ThrowsAsync<Exception>(() => service.PersistAndGetPayload());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task ThrowsWhenNotOkBytes()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\tservice.ProcessImageIsOk(new byte[1025], \"image/jpeg\");\n\n\t\t\tawait Assert.ThrowsAsync<Exception>(() => service.PersistAndGetPayload());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task PersistsImageAndTempRecordAndReturnsPayload()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar tenantID = \"pop\";\n\t\t\t_tenantService.GetTenant().Returns(tenantID);\n\t\t\t_settingsManager.Current.PostImageMaxkBytes.Returns(1);\n\t\t\tvar array = new byte[1];\n\t\t\t_imageService.ConstrainResize(Arg.Any<byte[]>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<int>(), false).Returns(array);\n\t\t\tvar guid = Guid.NewGuid();\n\t\t\tvar id = guid.ToString();\n\t\t\tvar payload = new PostImagePersistPayload {ID = id, Url = \"neat\"};\n\t\t\t_postImageRepository.Persist(Arg.Any<byte[]>(), \"image/jpeg\").Returns(Task.FromResult(payload));\n\t\t\tservice.ProcessImageIsOk(new byte[1], \"image/jpeg\");\n\n\t\t\tvar result = await service.PersistAndGetPayload();\n\n\t\t\tawait _postImageRepository.Received().Persist(array, \"image/jpeg\");\n\t\t\tawait _postImageTempRepository.Received().Save(guid, Arg.Any<DateTime>(), tenantID);\n\t\t\tAssert.Same(payload, result);\n\t\t}\n\t}\n\n\tpublic class DeleteTempRecord : PostImageServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task TempRepoCalledWithGuid()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar guid = Guid.NewGuid();\n\t\t\tvar id = guid.ToString();\n\n\t\t\tawait service.DeleteTempRecord(id);\n\n\t\t\tawait _postImageTempRepository.Received().Delete(guid);\n\t\t}\n\t}\n\n\tpublic class DeleteTempRecords : PostImageServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task TempRepoCalledWithGuidsFoundInText()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar guid = Guid.NewGuid();\n\t\t\tvar id = guid.ToString();\n\t\t\tvar guid2 = Guid.NewGuid();\n\t\t\tvar id2 = guid2.ToString();\n\t\t\tvar guid3 = Guid.NewGuid();\n\t\t\tvar id3 = guid3.ToString();\n\t\t\tvar array = new[] {id, id2, id3};\n\t\t\tvar text = $\"all the words {id3} and ids {id} {id2} \";\n\n\t\t\tawait service.DeleteTempRecords(array, text);\n\n\t\t\tawait _postImageTempRepository.Received().Delete(guid);\n\t\t\tawait _postImageTempRepository.Received().Delete(guid2);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task TempRepoCalledExcludingGuidsNotFoundInText()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar guid = Guid.NewGuid();\n\t\t\tvar id = guid.ToString();\n\t\t\tvar guid2 = Guid.NewGuid();\n\t\t\tvar id2 = guid2.ToString();\n\t\t\tvar guid3 = Guid.NewGuid();\n\t\t\tvar id3 = guid3.ToString();\n\t\t\tvar array = new[] { id, id2, id3 };\n\t\t\tvar text = $\"all the words and ids {id} {id3} \";\n\n\t\t\tawait service.DeleteTempRecords(array, text);\n\n\t\t\tawait _postImageTempRepository.Received().Delete(guid);\n\t\t\tawait _postImageTempRepository.DidNotReceive().Delete(guid2);\n\t\t\tawait _postImageTempRepository.Received().Delete(guid3);\n\t\t}\n\t}\n\n\tpublic class DeleteOldPostImages : PostImageServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task PostImageRepoCalledForEachEntry()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar tenantID = \"pop\";\n\t\t\tvar ids = new List<Guid> {Guid.NewGuid(), Guid.NewGuid()};\n\t\t\t_tenantService.GetTenant().Returns(tenantID);\n\t\t\t_postImageTempRepository.GetOld(Arg.Any<DateTime>()).Returns(Task.FromResult(ids));\n\n\t\t\tawait service.DeleteOldPostImages();\n\n\t\t\tawait _postImageRepository.Received().DeletePostImageData(ids[0].ToString(), tenantID);\n\t\t\tawait _postImageRepository.Received().DeletePostImageData(ids[1].ToString(), tenantID);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task PostImageTempRepoCalledForEachEntry()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar tenantID = \"pop\";\n\t\t\tvar ids = new List<Guid> { Guid.NewGuid(), Guid.NewGuid() };\n\t\t\t_tenantService.GetTenant().Returns(tenantID);\n\t\t\t_postImageTempRepository.GetOld(Arg.Any<DateTime>()).Returns(Task.FromResult(ids));\n\n\t\t\tawait service.DeleteOldPostImages();\n\n\t\t\tawait _postImageTempRepository.Received().Delete(ids[0]);\n\t\t\tawait _postImageTempRepository.Received().Delete(ids[1]);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/PostMasterServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class PostMasterServiceTests\n{\n\tprivate PostMasterService GetService()\n\t{\n\t\t_textParser = Substitute.For<ITextParsingService>();\n\t\t_topicRepo = Substitute.For<ITopicRepository>();\n\t\t_postRepo = Substitute.For<IPostRepository>();\n\t\t_forumRepo = Substitute.For<IForumRepository>();\n\t\t_profileRepo = Substitute.For<IProfileRepository>();\n\t\t_eventPublisher = Substitute.For<IEventPublisher>();\n\t\t_broker = Substitute.For<IBroker>();\n\t\t_searchIndexQueueRepo = Substitute.For<ISearchIndexQueueRepository>();\n\t\t_tenantService = Substitute.For<ITenantService>();\n\t\t_subscribedTopicsService = Substitute.For<ISubscribedTopicsService>();\n\t\t_moderationLogService = Substitute.For<IModerationLogService>();\n\t\t_forumPermissionService = Substitute.For<IForumPermissionService>();\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_topicViewCountService = Substitute.For<ITopicViewCountService>();\n\t\t_postImageService = Substitute.For<IPostImageService>();\n\t\treturn new PostMasterService(_textParser, _topicRepo, _postRepo, _forumRepo, _profileRepo, _eventPublisher, _broker, _searchIndexQueueRepo, _tenantService, _subscribedTopicsService, _moderationLogService, _forumPermissionService, _settingsManager, _topicViewCountService, _postImageService);\n\t}\n\n\tprivate ITextParsingService _textParser;\n\tprivate ITopicRepository _topicRepo;\n\tprivate IPostRepository _postRepo;\n\tprivate IForumRepository _forumRepo;\n\tprivate IProfileRepository _profileRepo;\n\tprivate IEventPublisher _eventPublisher;\n\tprivate IBroker _broker;\n\tprivate ISearchIndexQueueRepository _searchIndexQueueRepo;\n\tprivate ITenantService _tenantService;\n\tprivate ISubscribedTopicsService _subscribedTopicsService;\n\tprivate IModerationLogService _moderationLogService;\n\tprivate IForumPermissionService _forumPermissionService;\n\tprivate ISettingsManager _settingsManager;\n\tprivate ITopicViewCountService _topicViewCountService;\n\tprivate IPostImageService _postImageService;\n\n\tprivate async Task<User> DoUpNewTopic()\n\t{\n\t\tvar forum = new Forum { ForumID = 1 };\n\t\tvar user = GetUser();\n\t\tconst string ip = \"127.0.0.1\";\n\t\tconst string title = \"mah title\";\n\t\tconst string text = \"mah text\";\n\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1 };\n\t\tvar service = GetService();\n\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"parsed text\");\n\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t_postRepo.Create(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<bool>(), Arg.Any<string>(), null, Arg.Any<bool>(), Arg.Any<int>()).Returns(Task.FromResult(69));\n\t\t_forumRepo.Get(forum.ForumID).Returns(forum);\n\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t_topicRepo.Create(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<string>()).Returns(Task.FromResult(111));\n\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext {UserCanModerate = false, UserCanPost = true, UserCanView = true}));\n\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\tawait service.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\t\treturn user;\n\t}\n\t\t\n\tprivate User GetUser()\n\t{\n\t\tvar user = Models.UserTest.GetTestUser();\n\t\tuser.Roles = new List<string>();\n\t\treturn user;\n\t}\n\n\tpublic class PostNewTopicTests : PostMasterServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task NoUserReturnsFalseIsSuccess()\n\t\t{\n\t\t\tvar service = GetService();\n\n\t\t\tvar result = await service.PostNewTopic(null, new NewPost(), \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task UserWithoutPostPermissionReturnsFalseIsSuccess()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum{ForumID = 1};\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\tvar user = GetUser();\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext {DenialReason = Resources.ForumNoPost, UserCanModerate = false, UserCanPost = false, UserCanView = true}));\n\n\t\t\tvar result = await service.PostNewTopic(user, new NewPost {ItemID = forum.ForumID}, \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task UserWithoutViewPermissionReturnsFalseIsSuccess()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\tvar user = GetUser();\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { DenialReason = Resources.ForumNoView, UserCanModerate = false, UserCanPost = false, UserCanView = false }));\n\n\t\t\tvar result = await service.PostNewTopic(user, new NewPost { ItemID = forum.ForumID }, \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NoForumMatchThrows()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_forumRepo.Get(Arg.Any<int>()).Returns((Forum) null);\n\n\t\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.PostNewTopic(GetUser(), new NewPost {ItemID = 1}, \"\", \"\", x => \"\", x => \"\"));\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsPostRepoCreateWithPlainText()\n\t\t{\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1, IsPlainText = true };\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"html text\");\n\t\t\t_textParser.ForumCodeToHtml(\"mah text\").Returns(\"bb text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait topicService.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\n\t\t\tawait _postRepo.Received().Create(0, 0, ip, true, false, user.UserID, user.Name, \"parsed title\", \"bb text\", Arg.Any<DateTime>(), false, user.Name, null, false, 0);\n\t\t\tawait _topicRepo.Received().Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsPostRepoCreateWithHtmlText()\n\t\t{\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1, IsPlainText = false };\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"html text\");\n\t\t\t_textParser.ForumCodeToHtml(\"mah text\").Returns(\"bb text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait topicService.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\n\t\t\tawait _postRepo.Received().Create(0, 0, ip, true, false, user.UserID, user.Name, \"parsed title\", \"html text\", Arg.Any<DateTime>(), false, user.Name, null, false, 0);\n\t\t\tawait _topicRepo.Received().Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsPostRepoWithTopicID()\n\t\t{\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1, IsPlainText = false };\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"html text\");\n\t\t\t_textParser.ForumCodeToHtml(\"mah text\").Returns(\"bb text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_topicRepo.Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\").Returns(Task.FromResult(543));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait topicService.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\n\t\t\tawait _postRepo.Received().Create(543, 0, ip, true, false, user.UserID, user.Name, \"parsed title\", \"html text\", Arg.Any<DateTime>(), false, user.Name, null, false, 0);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsSubscribeServiceWithUserAndTopicIfEnabled()\n\t\t{\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1, IsPlainText = false };\n\t\t\tvar profile = new Profile {UserID = user.UserID, IsAutoFollowOnReply = true};\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"html text\");\n\t\t\t_textParser.ForumCodeToHtml(\"mah text\").Returns(\"bb text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_topicRepo.Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\").Returns(Task.FromResult(543));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(profile));\n\n\t\t\tawait topicService.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\t\t\t\n\t\t\tawait _subscribedTopicsService.Received().AddSubscribedTopic(user.UserID, 543);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsPostImageServiceWithIDs()\n\t\t{\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar postImageIDs = new string[] {Guid.NewGuid().ToString(), Guid.NewGuid().ToString()};\n\t\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1, IsPlainText = false, PostImageIDs = postImageIDs};\n\t\t\tvar profile = new Profile { UserID = user.UserID, IsAutoFollowOnReply = true };\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"html text\");\n\t\t\t_textParser.ForumCodeToHtml(\"mah text\").Returns(\"bb text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_topicRepo.Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\").Returns(Task.FromResult(543));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(profile));\n\n\t\t\tawait topicService.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\n\t\t\tawait _postImageService.Received().DeleteTempRecords(postImageIDs, newPost.FullText);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task DupeOfLastPostReturnsFalseIsSuccess()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\tvar user = GetUser();\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\tvar lastPost = \"last post text\";\n\t\t\tvar lastPostID = 456;\n\t\t\t_profileRepo.GetLastPostID(user.UserID).Returns(lastPostID);\n\t\t\t_postRepo.Get(lastPostID).Returns(Task.FromResult(new Post {FullText = lastPost, PostTime = DateTime.MinValue}));\n\t\t\t_textParser.ClientHtmlToHtml(lastPost).Returns(lastPost);\n\t\t\t_settingsManager.Current.MinimumSecondsBetweenPosts.Returns(9);\n\n\t\t\tvar result = await service.PostNewTopic(user, new NewPost { ItemID = forum.ForumID, FullText = lastPost }, \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(string.Format(Resources.PostWait, 9), result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task MinimumTimeBetweenPostsNotMetReturnsFalseIsSuccess()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\tvar user = GetUser();\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\tvar lastPost = \"last post text\";\n\t\t\tvar lastPostID = 456;\n\t\t\t_profileRepo.GetLastPostID(user.UserID).Returns(lastPostID);\n\t\t\t_postRepo.Get(lastPostID).Returns(Task.FromResult(new Post { FullText = lastPost, PostTime = DateTime.UtcNow }));\n\t\t\t_textParser.ClientHtmlToHtml(lastPost).Returns(lastPost);\n\t\t\t_settingsManager.Current.MinimumSecondsBetweenPosts.Returns(9);\n\n\t\t\tvar result = await service.PostNewTopic(user, new NewPost { ItemID = forum.ForumID, FullText = \"oiheorihgeorihg\" }, \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(string.Format(Resources.PostWait, 9), result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsTopicRepoCreate()\n\t\t{\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1 };\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"parsed text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait topicService.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\n\t\t\tawait _topicRepo.Received().Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task TitleIsParsed()\n\t\t{\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1 };\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"parsed text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait topicService.PostNewTopic(user, newPost, ip, default, _ => \"\", _ => \"\");\n\n\t\t\tawait _topicRepo.Received().Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsForumTopicPostIncrement()\n\t\t{\n\t\t\tawait DoUpNewTopic();\n\t\t\tawait _forumRepo.Received().IncrementPostAndTopicCount(1);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsForumUpdateLastUser()\n\t\t{\n\t\t\tvar user = await DoUpNewTopic();\n\t\t\tawait _forumRepo.Received().UpdateLastTimeAndUser(1, Arg.Any<DateTime>(), user.Name);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsProfileSetLastPost()\n\t\t{\n\t\t\tvar user = await DoUpNewTopic();\n\t\t\tawait _profileRepo.Received().SetLastPostID(user.UserID, 69);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task PublishesNewTopicEvent()\n\t\t{\n\t\t\tvar user = await DoUpNewTopic();\n\t\t\tawait _eventPublisher.Received().ProcessEvent(Arg.Any<string>(), user, EventDefinitionService.StaticEventIDs.NewTopic, false);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task PublishesNewPostEvent()\n\t\t{\n\t\t\tvar user = await DoUpNewTopic();\n\t\t\tawait _eventPublisher.Received().ProcessEvent(String.Empty, user, EventDefinitionService.StaticEventIDs.NewPost, true);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsBroker()\n\t\t{\n\t\t\tawait DoUpNewTopic();\n\t\t\t_broker.Received().NotifyForumUpdate(Arg.Any<Forum>());\n\t\t\t_broker.Received().NotifyTopicUpdate(Arg.Any<Topic>(), Arg.Any<Forum>(), Arg.Any<string>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task QueuesTopicForIndexing()\n\t\t{\n\t\t\tawait DoUpNewTopic();\n\t\t\t_tenantService.GetTenant().Returns(\"\");\n\t\t\tawait _searchIndexQueueRepo.Received().Enqueue(Arg.Any<SearchIndexPayload>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task DoesNotPublishToFeedIfForumHasViewRestrictions()\n\t\t{\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar newPost = new NewPost { Title = title, FullText = text, ItemID = 1 };\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string> { \"Admin\" }));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"parsed text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_topicRepo.Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\").Returns(Task.FromResult(2));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait topicService.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\n\t\t\tawait _eventPublisher.Received().ProcessEvent(Arg.Any<string>(), Arg.Any<User>(), EventDefinitionService.StaticEventIDs.NewTopic, true);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task ReturnsTopic()\n\t\t{\n\t\t\tvar forum = new Forum {ForumID = 1};\n\t\t\tvar user = GetUser();\n\t\t\tconst string ip = \"127.0.0.1\";\n\t\t\tconst string title = \"mah title\";\n\t\t\tconst string text = \"mah text\";\n\t\t\tvar newPost = new NewPost {Title = title, FullText = text, ItemID = 1};\n\t\t\tvar topicService = GetService();\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(forum.ForumID).Returns(Task.FromResult(new List<string>()));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"parsed-title\").Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"mah text\").Returns(\"parsed text\");\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_topicRepo.Create(forum.ForumID, \"parsed title\", 0, 0, user.UserID, user.Name, user.UserID, user.Name, Arg.Any<DateTime>(), false, false, false, \"parsed-title\").Returns(Task.FromResult(2));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanModerate = false, UserCanPost = true, UserCanView = true }));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tvar result = await topicService.PostNewTopic(user, newPost, ip, default, x => \"\", x => \"\");\n\n\t\t\tAssert.Equal(2, result.Data.TopicID);\n\t\t\tAssert.Equal(forum.ForumID, result.Data.ForumID);\n\t\t\tAssert.Equal(\"parsed title\", result.Data.Title);\n\t\t\tAssert.Equal(0, result.Data.ReplyCount);\n\t\t\tAssert.Equal(0, result.Data.ViewCount);\n\t\t\tAssert.Equal(user.UserID, result.Data.StartedByUserID);\n\t\t\tAssert.Equal(user.Name, result.Data.StartedByName);\n\t\t\tAssert.Equal(user.UserID, result.Data.LastPostUserID);\n\t\t\tAssert.Equal(user.Name, result.Data.LastPostName);\n\t\t\tAssert.False(result.Data.IsClosed);\n\t\t\tAssert.False(result.Data.IsDeleted);\n\t\t\tAssert.False(result.Data.IsPinned);\n\t\t\tAssert.Equal(\"parsed-title\", result.Data.UrlName);\n\t\t}\n\t}\n\n\tpublic class PostReplyTests : PostMasterServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task NoUserReturnsFalseIsSuccessful()\n\t\t{\n\t\t\tvar service = GetService();\n\n\t\t\tvar result = await service.PostReply(null, 0, \"\", false, new NewPost(), DateTime.MaxValue, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NoTopicReturnsFalseIsSuccessful()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_topicRepo.Get(Arg.Any<int>()).Returns((Topic) null);\n\n\t\t\tvar result = await service.PostReply(GetUser(), 0, \"\", false, new NewPost(), DateTime.MaxValue, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(Resources.TopicNotExist, result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NoForumThrows()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_topicRepo.Get(Arg.Any<int>()).Returns(Task.FromResult(new Topic()));\n\t\t\t_forumRepo.Get(Arg.Any<int>()).Returns((Forum) null);\n\n\t\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.PostReply(GetUser(), 0, \"\", false, new NewPost(), DateTime.MaxValue, (t) => \"\", \"\", x => \"\", x => \"\"));\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NoViewPermissionReturnsFalseIsSuccessful()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar user = GetUser();\n\t\t\tvar forum = new Forum {ForumID = 1};\n\t\t\tvar topic = new Topic {ForumID = forum.ForumID};\n\t\t\tvar newPost = new NewPost {ItemID = topic.TopicID};\n\t\t\t_topicRepo.Get(Arg.Any<int>()).Returns(Task.FromResult(topic));\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext {UserCanView = false, UserCanPost = true}));\n\n\t\t\tvar result = await service.PostReply(user, 0, \"\", false, newPost, DateTime.MaxValue, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(Resources.ForumNoView, result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NoPostPermissionReturnsFalseIsSuccessful()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar user = GetUser();\n\t\t\tvar forum = new Forum { ForumID = 1 };\n\t\t\tvar topic = new Topic { ForumID = forum.ForumID };\n\t\t\tvar newPost = new NewPost { ItemID = topic.TopicID };\n\t\t\t_topicRepo.Get(Arg.Any<int>()).Returns(Task.FromResult(topic));\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanView = true, UserCanPost = false }));\n\n\t\t\tvar result = await service.PostReply(user, 0, \"\", false, newPost, DateTime.MaxValue, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(Resources.ForumNoPost, result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task ClosedTopicReturnsFalseIsSuccessful()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_topicRepo.Get(Arg.Any<int>()).Returns(Task.FromResult(new Topic{IsClosed = true}));\n\n\t\t\tvar result = await service.PostReply(GetUser(), 0, \"\", false, new NewPost(), DateTime.MaxValue,(t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(Resources.Closed, result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task UsesPlainTextParsed()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, Title = \"\" };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ForumCodeToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID, IsPlainText = true };\n\t\t\t_textParser.Censor(newPost.Title).Returns(\"parsed title\");\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _postRepo.Received().Create(topic.TopicID, 0, \"127.0.0.1\", false, true, user.UserID, user.Name, \"parsed title\", \"parsed text\", postTime, false, user.Name, null, false, 0);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task UsesRichTextParsed()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, Title = \"\" };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID, IsPlainText = false };\n\t\t\t_textParser.Censor(newPost.Title).Returns(\"parsed title\");\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _postRepo.Received().Create(topic.TopicID, 0, \"127.0.0.1\", false, true, user.UserID, user.Name, \"parsed title\", \"parsed text\", postTime, false, user.Name, null, false, 0);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task DupeOfLastPostFails()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, Title = \"\" };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetLastPostID(user.UserID).Returns(654);\n\t\t\t_postRepo.Get(654).Returns(Task.FromResult(new Post {FullText = \"parsed text\", PostTime = DateTime.MinValue}));\n\t\t\t_settingsManager.Current.MinimumSecondsBetweenPosts.Returns(9);\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID, IsPlainText = false };\n\n\t\t\tvar result = await service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(string.Format(Resources.PostWait, 9), result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task MinTimeSinceLastPostTooShortFails()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, Title = \"\" };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"oihf text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetLastPostID(user.UserID).Returns(654);\n\t\t\t_postRepo.Get(654).Returns(Task.FromResult(new Post { FullText = \"parsed text\", PostTime = DateTime.UtcNow }));\n\t\t\t_settingsManager.Current.MinimumSecondsBetweenPosts.Returns(9);\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID, IsPlainText = false };\n\n\t\t\tvar result = await service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(string.Format(Resources.PostWait, 9), result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task EmptyPostFails()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, Title = \"\" };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetLastPostID(user.UserID).Returns(654);\n\t\t\t_settingsManager.Current.MinimumSecondsBetweenPosts.Returns(9);\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID, IsPlainText = false };\n\n\t\t\tvar result = await service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime,  (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(Resources.PostEmpty, result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task HitsRepo()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, Title = \"\" };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\t\t\t_textParser.Censor(newPost.Title).Returns(\"parsed title\");\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime,  (_) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _postRepo.Received().Create(topic.TopicID, 0, \"127.0.0.1\", false, true, user.UserID, user.Name, \"parsed title\", \"parsed text\", postTime, false, user.Name, null, false, 0);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task HitsSubscribedService()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\tvar tenandID = \"cb\";\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\t\t\t_tenantService.GetTenant().Returns(tenandID);\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _subscribedTopicsService.Received().NotifySubscribers(topic, user, tenandID);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task HitsSubscribeAddWhenProfileCallsForIt()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\tvar profile = new Profile {UserID = user.UserID, IsAutoFollowOnReply = true};\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(profile));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _subscribedTopicsService.Received().AddSubscribedTopic(user.UserID, topic.TopicID);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task IncrementsTopicReplyCount()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _topicRepo.Received().IncrementReplyCount(1);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task IncrementsForumPostCount()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _forumRepo.Received().IncrementPostCount(2);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task UpdatesTopicLastInfo()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _topicRepo.Received().UpdateLastTimeAndUser(topic.TopicID, user.UserID, user.Name, postTime);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task UpdatesForumLastInfo()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _forumRepo.Received().UpdateLastTimeAndUser(topic.ForumID, postTime, user.Name);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task PostQueuesMarksTopicForIndexing()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum {ForumID = topic.ForumID};\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext {UserCanPost = true, UserCanView = true}));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_tenantService.GetTenant().Returns(\"\");\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime,(t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _searchIndexQueueRepo.Received().Enqueue(Arg.Any<SearchIndexPayload>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NotifiesBroker()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.Get(topic.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\t_broker.Received().NotifyForumUpdate(forum);\n\t\t\t_broker.Received().NotifyTopicUpdate(topic, forum, Arg.Any<string>());\n\t\t\t_broker.Received().NotifyNewPost(topic, Arg.Any<int>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task SetsProfileLastPostID()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tvar result = await service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _profileRepo.Received().SetLastPostID(user.UserID, result.Data.PostID);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CallsPostImageServiceWithIDs()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\tvar postImageIDs = new string[] {Guid.NewGuid().ToString(), Guid.NewGuid().ToString()};\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID, PostImageIDs = postImageIDs};\n\n\t\t\tvar result = await service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _postImageService.Received().DeleteTempRecords(postImageIDs, newPost.FullText);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task PublishesEvent()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime,  (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _eventPublisher.Received().ProcessEvent(Arg.Any<string>(), user, EventDefinitionService.StaticEventIDs.NewPost, false);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task DoesNotPublishEventOnViewRestrictedForum()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1, ForumID = 2 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\tvar forum = new Forum {ForumID = topic.ForumID};\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string> { \"Admin\" }));\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tawait service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tawait _eventPublisher.Received().ProcessEvent(Arg.Any<string>(), user, EventDefinitionService.StaticEventIDs.NewPost, true);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task ReturnsHydratedObject()\n\t\t{\n\t\t\tvar topic = new Topic { TopicID = 1 };\n\t\t\tvar user = GetUser();\n\t\t\tvar postTime = DateTime.UtcNow;\n\t\t\tvar service = GetService();\n\t\t\t_forumRepo.GetForumViewRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_postRepo.Create(topic.TopicID, 0, \"127.0.0.1\", false, true, user.UserID, user.Name, \"parsed title\", \"parsed text\", Arg.Any<DateTime>(), false, Arg.Any<string>(), null, false, 0).Returns(Task.FromResult(123));\n\t\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t\t_forumPermissionService.GetPermissionContext(forum, user).Returns(Task.FromResult(new ForumPermissionContext { UserCanPost = true, UserCanView = true }));\n\t\t\t_textParser.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed text\");\n\t\t\t_forumRepo.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\t\t_textParser.Censor(\"mah title\").Returns(\"parsed title\");\n\t\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile()));\n\t\t\tvar newPost = new NewPost { FullText = \"mah text\", Title = \"mah title\", IncludeSignature = true, ItemID = topic.TopicID };\n\n\t\t\tvar result = await service.PostReply(user, 0, \"127.0.0.1\", false, newPost, postTime, (t) => \"\", \"\", x => \"\", x => \"\");\n\n\t\t\tAssert.Equal(topic.TopicID, result.Data.TopicID);\n\t\t\tAssert.Equal(\"parsed text\", result.Data.FullText);\n\t\t\tAssert.Equal(\"127.0.0.1\", result.Data.IP);\n\t\t\tAssert.False(result.Data.IsDeleted);\n\t\t\tAssert.False(result.Data.IsEdited);\n\t\t\tAssert.False(result.Data.IsFirstInTopic);\n\t\t\tAssert.Equal(user.Name, result.Data.LastEditName);\n\t\t\tAssert.Null(result.Data.LastEditTime);\n\t\t\tAssert.Equal(user.Name, result.Data.Name);\n\t\t\tAssert.Equal(0, result.Data.ParentPostID);\n\t\t\tAssert.Equal(123, result.Data.PostID);\n\t\t\tAssert.Equal(postTime, result.Data.PostTime);\n\t\t\tAssert.True(result.Data.ShowSig);\n\t\t\tAssert.Equal(\"parsed title\", result.Data.Title);\n\t\t\tAssert.Equal(user.UserID, result.Data.UserID);\n\t\t}\n\t}\n\n\tpublic class EditPostTests : PostMasterServiceTests\n\t{\n\t\tprivate new User GetUser()\n\t\t{\n\t\t\treturn new User {UserID = 123, Roles = new List<string>()};\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task FailsBecauseNoUserMatch()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post {UserID = 789}));\n\n\t\t\tvar result = await service.EditPost(456, new PostEdit(), new User {UserID = 111, Roles = new List<string>()}, x => \"\");\n\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t\tAssert.Equal(Resources.Forbidden, result.Message);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task CensorsTitle()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, UserID = 123 }));\n\t\t\tawait service.EditPost(456, new PostEdit { Title = \"blah\" }, GetUser(), x => \"\");\n\t\t\t_textParser.Received(1).Censor(\"blah\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NoTitleUpdateWhenNotFirstPostInTopic()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, UserID = 123, IsFirstInTopic = false, Title = \"blah\" }));\n\t\t\t_textParser.Censor(\"blah\").Returns(\"changed\");\n\n\t\t\tawait service.EditPost(456, new PostEdit { Title = \"blah\" }, GetUser(), x => \"\");\n\n\t\t\tawait _topicRepo.DidNotReceive().UpdateTitleAndForum(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<string>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NoTitleUpdateWhenTitleHasNotChanged()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, UserID = 123, IsFirstInTopic = true, Title = \"blah\" }));\n\t\t\t_textParser.Censor(\"blah\").Returns(\"blah\");\n\n\t\t\tawait service.EditPost(456, new PostEdit { Title = \"blah\" }, GetUser(), x => \"\");\n\n\t\t\tawait _topicRepo.DidNotReceive().UpdateTitleAndForum(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<string>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task NoEditWhenTitleIsEmpty()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, UserID = 123, IsFirstInTopic = true, Title = \"blah\" }));\n\t\t\t_textParser.Censor(\"blah\").Returns(\"\");\n\n\t\t\tvar result = await service.EditPost(456, new PostEdit { Title = \"blah\" }, GetUser(), x => \"\");\n\n\t\t\tawait _topicRepo.DidNotReceive().UpdateTitleAndForum(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<string>());\n\t\t\tAssert.False(result.IsSuccessful);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task TitleUpdateWhenFirstPostInTopicAndTitleChanged()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, TopicID = 222, UserID = 123, IsFirstInTopic = true, Title = \"blah\" }));\n\t\t\t_topicRepo.Get(222).Returns(Task.FromResult(new Topic {TopicID = 222, ForumID = 111}));\n\t\t\t_forumRepo.Get(111).Returns(Task.FromResult(new Forum {ForumID = 111}));\n\t\t\t_textParser.Censor(\"blah\").Returns(\"changed\");\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(\"changed\").Returns(Task.FromResult(new List<string>()));\n\n\t\t\tawait service.EditPost(456, new PostEdit { Title = \"blah\" }, GetUser(), x => \"\");\n\n\t\t\tawait _topicRepo.Received().UpdateTitleAndForum(222, 111, \"changed\", \"changed\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task PlainTextParsed()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, UserID = 123 }));\n\t\t\tawait service.EditPost(456, new PostEdit { FullText = \"blah\", IsPlainText = true }, GetUser(), x => \"\");\n\t\t\t_textParser.Received(1).ForumCodeToHtml(\"blah\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task RichTextParsed()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, UserID = 123 }));\n\n\t\t\tawait service.EditPost(456, new PostEdit { FullText = \"blah\", IsPlainText = false }, GetUser(), x => \"\");\n\n\t\t\t_textParser.Received(1).ClientHtmlToHtml(\"blah\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task SavesMappedValues()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar post = new Post { PostID = 67, IsFirstInTopic = true };\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, UserID = 123, IsFirstInTopic = true }));\n\t\t\tawait _postRepo.Update(Arg.Do<Post>(p => post = p));\n\t\t\t_topicRepo.Get(post.TopicID).Returns(Task.FromResult(new Topic {ForumID = 333}));\n\t\t\t_forumRepo.Get(333).Returns(Task.FromResult(new Forum {ForumID = 333}));\n\t\t\t_topicRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\t\t_textParser.ClientHtmlToHtml(\"blah\").Returns(\"new\");\n\t\t\t_textParser.Censor(\"unparsed title\").Returns(\"new title\");\n\n\t\t\tvar result = await service.EditPost(456, new PostEdit { FullText = \"blah\", Title = \"unparsed title\", IsPlainText = false, ShowSig = true, IsFirstInTopic = true }, new User { UserID = 123, Name = \"dude\", Roles = new List<string>()}, x => \"\");\n\n\t\t\tAssert.True(result.IsSuccessful);\n\t\t\tAssert.NotEqual(post.LastEditTime, new DateTime(2009, 1, 1));\n\t\t\tAssert.Equal(456, post.PostID);\n\t\t\tAssert.Equal(\"new\", post.FullText);\n\t\t\tAssert.Equal(\"new title\", post.Title);\n\t\t\tAssert.True(post.ShowSig);\n\t\t\tAssert.True(post.IsEdited);\n\t\t\tAssert.Equal(\"dude\", post.LastEditName);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task ModeratorLogged()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar user = new User { UserID = 123, Name = \"dude\", Roles = new List<string>()};\n\t\t\t_textParser.ClientHtmlToHtml(\"blah\").Returns(\"new\");\n\t\t\t_textParser.Censor(\"unparsed title\").Returns(\"new title\");\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post{PostID = 456, UserID = user.UserID, FullText = \"old text\"}));\n\n\t\t\tvar result = await service.EditPost(456, new PostEdit { FullText = \"blah\", Title = \"unparsed title\", IsPlainText = false, ShowSig = true, Comment = \"mah comment\" }, user, x => \"\");\n\n\t\t\tAssert.True(result.IsSuccessful);\n\t\t\tawait _moderationLogService.Received(1).LogPost(user, ModerationType.PostEdit, Arg.Any<Post>(), \"mah comment\", \"old text\");\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task QueuesTopicForIndexing()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar user = new User { UserID = 123, Name = \"dude\", Roles = new List<string>() };\n\t\t\tvar post = new Post { PostID = 456, ShowSig = false, FullText = \"old text\", TopicID = 999, UserID = user.UserID };\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(post));\n\t\t\t_tenantService.GetTenant().Returns(\"\");\n\n\t\t\tvar result = await service.EditPost(456, new PostEdit { FullText = \"blah\", Title = \"unparsed title\", IsPlainText = false, ShowSig = true, Comment = \"mah comment\" }, user, x => \"\");\n\n\t\t\tAssert.True(result.IsSuccessful);\n\t\t\tawait _searchIndexQueueRepo.Received().Enqueue(Arg.Any<SearchIndexPayload>());\n\t\t}\n\n\t\t[Fact]\n\t\tpublic async Task ModeratorCanEdit()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar post = new Post { PostID = 67 };\n\t\t\t_postRepo.Get(456).Returns(Task.FromResult(new Post { PostID = 456, UserID = 123 }));\n\t\t\tawait _postRepo.Update(Arg.Do<Post>(x => post = x));\n\t\t\t_textParser.ClientHtmlToHtml(\"blah\").Returns(\"new\");\n\t\t\t_textParser.Censor(\"unparsed title\").Returns(\"new title\");\n\n\t\t\tvar result = await service.EditPost(456, new PostEdit { FullText = \"blah\", Title = \"unparsed title\", IsPlainText = false, ShowSig = true }, new User { UserID = 123, Name = \"dude\", Roles = new List<string>(new []{PermanentRoles.Moderator})}, x => \"\");\n\n\t\t\tAssert.True(result.IsSuccessful);\n\t\t\tawait _postRepo.Received().Update(Arg.Any<Post>());\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/PostServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class PostServiceTests\n{\n\tprivate IPostRepository _postRepo;\n\tprivate IProfileRepository _profileRepo;\n\tprivate ISettingsManager _settingsManager;\n\tprivate Settings _settings;\n\tprivate ITopicService _topicService;\n\tprivate ITextParsingService _textParsingService;\n\tprivate IModerationLogService _modLogService;\n\tprivate IForumService _forumService;\n\tprivate IEventPublisher _eventPub;\n\tprivate IUserService _userService;\n\tprivate ISearchIndexQueueRepository _searchIndexQueue;\n\tprivate ITenantService _tenantService;\n\tprivate INotificationAdapter _notificationAdapter;\n\n\tprivate PostService GetService()\n\t{\n\t\t_postRepo = Substitute.For<IPostRepository>();\n\t\t_profileRepo = Substitute.For<IProfileRepository>();\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_settings = Substitute.For<Settings>();\n\t\t_topicService = Substitute.For<ITopicService>();\n\t\t_textParsingService = Substitute.For<ITextParsingService>();\n\t\t_modLogService = Substitute.For<IModerationLogService>();\n\t\t_forumService = Substitute.For<IForumService>();\n\t\t_eventPub = Substitute.For<IEventPublisher>();\n\t\t_userService = Substitute.For<IUserService>();\n\t\t_searchIndexQueue = Substitute.For<ISearchIndexQueueRepository>();\n\t\t_tenantService = Substitute.For<ITenantService>();\n\t\t_notificationAdapter = Substitute.For<INotificationAdapter>();\n\t\t_settingsManager.Current.Returns(_settings);\n\t\treturn new PostService(_postRepo, _profileRepo, _settingsManager, _topicService, _textParsingService, _modLogService, _forumService, _eventPub, _userService, _searchIndexQueue, _tenantService, _notificationAdapter);\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostsPageSizeAndStartRowCalcdCorrectly()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ReplyCount = 20};\n\t\tvar postService = GetService();\n\t\t_settings.PostsPerPage.Returns(2);\n\t\tvar (_, pagerContext) = await postService.GetPosts(topic, false, 4);\n\t\tawait _postRepo.Received().Get(1, false, 7, 2);\n\t\tawait _postRepo.DidNotReceive().GetReplyCount(Arg.Any<int>(), Arg.Any<bool>());\n\t\tAssert.Equal(11, pagerContext.PageCount);\n\t\tAssert.Equal(2, pagerContext.PageSize);\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostsReplyCountCalledOnIncludeDeleted()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ReplyCount = 20 };\n\t\tvar postService = GetService();\n\t\t_settings.PostsPerPage.Returns(2);\n\t\t_postRepo.GetReplyCount(topic.TopicID, true).Returns(Task.FromResult(21));\n\t\tvar (_, pagerContext) = await postService.GetPosts(topic, true, 4);\n\t\tawait _postRepo.Received().GetReplyCount(topic.TopicID, true);\n\t\tAssert.Equal(11, pagerContext.PageCount);\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostsPagerContextConstructed()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ReplyCount = 20 };\n\t\tvar postService = GetService();\n\t\t_settings.PostsPerPage.Returns(3);\n\t\tvar (_, pagerContext) = await postService.GetPosts(topic, false, 4);\n\t\tAssert.Equal(7, pagerContext.PageCount);\n\t\tAssert.Equal(4, pagerContext.PageIndex);\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostsHitsRepo()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ReplyCount = 20 };\n\t\tvar posts = new List<Post>();\n\t\tvar postService = GetService();\n\t\t_settings.PostsPerPage.Returns(3);\n\t\t_postRepo.Get(1, false, Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(posts));\n\t\tvar result = await postService.GetPosts(topic, false, 4);\n\t\tAssert.Same(posts, result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task GetCallsRepoAndReturns()\n\t{\n\t\tvar postService = GetService();\n\t\tvar postID = 123;\n\t\tvar post = new Post {PostID = postID};\n\t\t_postRepo.Get(postID).Returns(Task.FromResult(post));\n\t\tvar postResult = await postService.Get(postID);\n\t\tawait _postRepo.Received().Get(postID);\n\t\tAssert.Same(postResult, post);\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostCountCallsRepo()\n\t{\n\t\tvar postService = GetService();\n\t\t_postRepo.GetPostCount(123).Returns(Task.FromResult(456));\n\t\tvar user = new User { UserID = 123 };\n\t\tvar result = await postService.GetPostCount(user);\n\t\tawait _postRepo.Received(1).GetPostCount(123);\n\t\tAssert.Equal(456, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostForEditPlainText()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new Post { PostID = 123, Title = \"mah title\", FullText = \"not\", ShowSig = true, IsFirstInTopic = true };\n\t\tvar user = new User { UserID = 456 };\n\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile {IsPlainText = true}));\n\t\t_textParsingService.HtmlToForumCode(\"not\").Returns(\"new text\");\n\t\tvar postEdit = await service.GetPostForEdit(post, user);\n\t\tAssert.Equal(\"mah title\", postEdit.Title);\n\t\tAssert.Equal(\"new text\", postEdit.FullText);\n\t\tAssert.True(postEdit.IsFirstInTopic);\n\t\tAssert.True(postEdit.ShowSig);\n\t\tAssert.True(postEdit.IsPlainText);\n\t\t_textParsingService.Received(1).HtmlToForumCode(\"not\");\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostForEditNotPlainText()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new Post { PostID = 123, Title = \"mah title\", FullText = \"not\", ShowSig = true, IsFirstInTopic = true };\n\t\tvar user = new User { UserID = 456 };\n\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(new Profile { IsPlainText = false }));\n\t\t_textParsingService.HtmlToClientHtml(\"not\").Returns(\"new text\");\n\t\tvar postEdit = await service.GetPostForEdit(post, user);\n\t\tAssert.Equal(\"mah title\", postEdit.Title);\n\t\tAssert.Equal(\"new text\", postEdit.FullText);\n\t\tAssert.True(postEdit.IsFirstInTopic);\n\t\tAssert.True(postEdit.ShowSig);\n\t\tAssert.False(postEdit.IsPlainText);\n\t\t_textParsingService.Received(1).HtmlToClientHtml(\"not\");\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteThrowsForNonAuthorAndNonMod()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar post = new Post { PostID = 67 };\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.Delete(post, user));\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteCallDeleteTopicIfFirstInTopic()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar post = new Post { PostID = 67, UserID = user.UserID, IsFirstInTopic = true, TopicID = topic.TopicID };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tawait service.Delete(post, user);\n\t\tawait _topicService.Received(1).DeleteTopic(topic, user);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteCallLogs()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar post = new Post { PostID = 67, UserID = user.UserID, IsFirstInTopic = false, TopicID = topic.TopicID };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tawait service.Delete(post, user);\n\t\tawait _modLogService.Received(1).LogPost(user, ModerationType.PostDelete, post, String.Empty, String.Empty);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteSetsEditFields()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar post = new Post { PostID = 67, UserID = user.UserID, IsFirstInTopic = false, TopicID = topic.TopicID, IsEdited = false };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tvar editedPost = new Post();\n\t\tawait _postRepo.Update(Arg.Do<Post>(x => editedPost = x));\n\t\tawait service.Delete(post, user);\n\t\tAssert.True(editedPost.IsEdited);\n\t\tAssert.Equal(user.Name, editedPost.LastEditName);\n\t\tAssert.True(editedPost.LastEditTime.HasValue);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteCallSetsIsDeletedAndUpdates()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar post = new Post { PostID = 67, UserID = user.UserID, IsFirstInTopic = false, TopicID = topic.TopicID, IsDeleted = false };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tvar persistedPost = new Post();\n\t\tawait _postRepo.Update(Arg.Do<Post>(x => persistedPost = x));\n\t\tawait service.Delete(post, user);\n\t\tAssert.Equal(post.PostID, persistedPost.PostID);\n\t\tAssert.True(persistedPost.IsDeleted);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteCallFiresRecalcs()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar post = new Post { PostID = 67, UserID = user.UserID, IsFirstInTopic = false, TopicID = topic.TopicID };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tvar payload = new SearchIndexPayload();\n\t\tawait _searchIndexQueue.Enqueue(Arg.Do<SearchIndexPayload>(x => payload = x));\n\n\t\tawait service.Delete(post, user);\n\n\t\tawait _topicService.Received(1).RecalculateReplyCount(topic);\n\t\tawait _topicService.Received().UpdateLast(topic);\n\t\t_forumService.Received(1).UpdateCounts(forum);\n\t\tawait _forumService.Received(1).UpdateLast(forum);\n\t\tawait _searchIndexQueue.Received().Enqueue(payload);\n\t\tAssert.Equal(topic.TopicID, payload.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteThrowsForNonMod()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar post = new Post { PostID = 67, UserID = user.UserID };\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await service.Undelete(post, user));\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteCallLogs()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123, Roles = new List<string> { PermanentRoles.Moderator }};\n\t\tvar post = new Post { PostID = 67, TopicID = topic.TopicID };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tawait service.Undelete(post, user);\n\t\tawait _modLogService.Received(1).LogPost(user, ModerationType.PostUndelete, post, String.Empty, String.Empty);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteSetsEditFields()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123, Roles = new List<string> { PermanentRoles.Moderator } };\n\t\tvar post = new Post { PostID = 67, TopicID = topic.TopicID, IsEdited = false, UserID = user.UserID };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tvar editedPost = new Post();\n\t\tawait _postRepo.Update(Arg.Do<Post>(x => editedPost = x));\n\t\tawait service.Undelete(post, user);\n\t\tAssert.True(editedPost.IsEdited);\n\t\tAssert.Equal(user.Name, editedPost.LastEditName);\n\t\tAssert.True(editedPost.LastEditTime.HasValue);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteCallSetsIsDeletedAndUpdates()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123, Roles = new List<string> { PermanentRoles.Moderator } };\n\t\tvar post = new Post { PostID = 67, TopicID = topic.TopicID, IsDeleted = true };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tvar persistedPost = new Post();\n\t\tawait _postRepo.Update(Arg.Do<Post>(x => persistedPost = x));\n\t\tawait service.Undelete(post, user);\n\t\tAssert.Equal(post.PostID, persistedPost.PostID);\n\t\tAssert.False(persistedPost.IsDeleted);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteCallFiresRecalcs()\n\t{\n\t\tvar forum = new Forum { ForumID = 5 };\n\t\tvar topic = new Topic { TopicID = 4, ForumID = forum.ForumID };\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123, Roles = new List<string> { PermanentRoles.Moderator } };\n\t\tvar post = new Post { PostID = 67, TopicID = topic.TopicID };\n\t\t_topicService.Get(topic.TopicID).Returns(Task.FromResult(topic));\n\t\t_forumService.Get(forum.ForumID).Returns(Task.FromResult(forum));\n\t\tvar payload = new SearchIndexPayload();\n\t\tawait _searchIndexQueue.Enqueue(Arg.Do<SearchIndexPayload>(x => payload = x));\n\n\t\tawait service.Undelete(post, user);\n\n\t\tawait _topicService.Received(1).RecalculateReplyCount(topic);\n\t\tawait _topicService.Received().UpdateLast(topic);\n\t\t_forumService.Received(1).UpdateCounts(forum);\n\t\tawait _forumService.Received(1).UpdateLast(forum);\n\t\tawait _searchIndexQueue.Received().Enqueue(payload);\n\t\tAssert.Equal(topic.TopicID, payload.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostsToEndSkipsLoadedPosts()\n\t{\n\t\tvar service = GetService();\n\t\tvar posts = new List<Post> {new Post { PostID = 1 }, new Post { PostID = 2 }, new Post { PostID = 3 }, new Post { PostID = 4 } };\n\t\t_postRepo.Get(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.FromResult(posts));\n\t\t_settingsManager.Current.PostsPerPage.Returns(5);\n\t\tvar result = await service.GetPosts(new Topic { TopicID = 123 }, 2, true);\n\t\tAssert.Equal(2, result.Item1.Count);\n\t\tAssert.Equal(3, result.Item1[0].PostID);\n\t\tAssert.Equal(4, result.Item1[1].PostID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetPostsToEndCalcsCorrectPagerContext()\n\t{\n\t\tvar service = GetService();\n\t\tvar posts = new List<Post> { new Post { PostID = 1 }, new Post { PostID = 2 }, new Post { PostID = 3 }, new Post { PostID = 4 }, new Post { PostID = 5 }, new Post { PostID = 6 }, new Post { PostID = 7 }, new Post { PostID = 8 } };\n\t\t_postRepo.Get(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.FromResult(posts));\n\t\t_settingsManager.Current.PostsPerPage.Returns(3);\n\t\tvar (_, pagerContext) = await service.GetPosts(new Topic { TopicID = 123 }, 2, true);\n\t\tAssert.Equal(3, pagerContext.PageCount);\n\t\tAssert.Equal(3, pagerContext.PageIndex);\n\t}\n\n\t[Fact]\n\tpublic async Task ToggleVoteCallsRepoWithPostIDAndUserID()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new Post { PostID = 1 };\n\t\tvar user = new User { UserID = 2 };\n\t\t_postRepo.GetVotes(post.PostID).Returns(Task.FromResult(new Dictionary<int, string>()));\n\t\tawait service.ToggleVoteReturnCountAndIsVoted(post, user, \"abc\", \"def\", \"ghi\");\n\t\tawait _postRepo.Received().VotePost(post.PostID, user.UserID);\n\t}\n\n\t[Fact]\n\tpublic async Task ToggleVoteCalcsAndSetsCount()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new Post { PostID = 1 };\n\t\tvar user = new User { UserID = 2 };\n\t\tconst int votes = 32;\n\t\t_postRepo.GetVotes(post.PostID).Returns(Task.FromResult(new Dictionary<int, string>()));\n\t\t_postRepo.CalculateVoteCount(post.PostID).Returns(Task.FromResult(votes));\n\t\tawait service.ToggleVoteReturnCountAndIsVoted(post, user, \"abc\", \"def\", \"ghi\");\n\t\tawait _postRepo.Received().SetVoteCount(post.PostID, votes);\n\t}\n\n\t[Fact]\n\tpublic async Task GetVoteCountCallsRepoAndReturns()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new Post { PostID = 1 };\n\t\tconst int votes = 32;\n\t\t_postRepo.GetVoteCount(post.PostID).Returns(Task.FromResult(votes));\n\t\tvar result = await service.GetVoteCount(post);\n\t\tAssert.Equal(votes, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetVotersReturnsContainerWithPostID()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new Post { PostID = 1 };\n\t\t_postRepo.GetVotes(post.PostID).Returns(Task.FromResult(new Dictionary<int, string>()));\n\t\tvar result = await service.GetVoters(post);\n\t\tAssert.Equal(post.PostID, result.PostID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetVotersReturnsContainerWithTotalVotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new Post { PostID = 1 };\n\t\tvar voters = new Dictionary<int, string> {{1, \"Foo\"}, {2, \"Dude\"}, {3, null}, {4, \"Chica\"}};\n\t\t_postRepo.GetVotes(post.PostID).Returns(Task.FromResult(voters));\n\t\tvar result = await service.GetVoters(post);\n\t\tAssert.Equal(4, result.Votes);\n\t}\n\n\t[Fact]\n\tpublic async Task GetVotersFiltersNullNames()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new Post { PostID = 1 };\n\t\tvar voters = new Dictionary<int, string> { { 1, \"Foo\" }, { 2, \"Dude\" }, { 3, null }, { 4, \"Chica\" } };\n\t\t_postRepo.GetVotes(post.PostID).Returns(Task.FromResult(voters));\n\t\tvar result = await service.GetVoters(post);\n\t\tAssert.Equal(3, result.Voters.Count);\n\t\tAssert.False(result.Voters.ContainsValue(null));\n\t}\n\n\t[Fact]\n\tpublic async Task GetVotedIDsPassesUserID()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tawait service.GetVotedPostIDs(user, new List<Post>());\n\t\tawait _postRepo.Received().GetVotedPostIDs(user.UserID, Arg.Any<List<int>>());\n\t}\n\n\t[Fact]\n\tpublic async Task GetVotedIDsPassesPostIDList()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar list = new List<Post> {new Post { PostID = 4 }, new Post { PostID = 5 }, new Post { PostID = 8 } };\n\t\tList<int> returnedList = null;\n\t\tawait _postRepo.GetVotedPostIDs(user.UserID, Arg.Do<List<int>>(x => returnedList = x));\n\t\tawait service.GetVotedPostIDs(user, list);\n\t\tAssert.Equal(3, returnedList.Count);\n\t\tAssert.Equal(4, returnedList[0]);\n\t\tAssert.Equal(5, returnedList[1]);\n\t\tAssert.Equal(8, returnedList[2]);\n\t}\n\n\t[Fact]\n\tpublic async Task GetVotedIDsReturnsRepoObject()\n\t{\n\t\tvar service = GetService();\n\t\tvar list = new List<int>();\n\t\t_postRepo.GetVotedPostIDs(Arg.Any<int>(), Arg.Any<List<int>>()).Returns(Task.FromResult(list));\n\t\tvar result = await service.GetVotedPostIDs(new User { UserID = 123 }, new List<Post>());\n\t\tAssert.Same(list, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetVotedIDsReturnsEmptyListWithNullUser()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = await service.GetVotedPostIDs(null, new List<Post>());\n\t\tAssert.Empty(result);\n\t}\n\n\t[Fact]\n\tpublic async Task ToggleVoteDoesntCallPublisherWhenUserFromPostDoesNotExist()\n\t{\n\t\tvar service = GetService();\n\t\t_userService.GetUser(Arg.Any<int>()).Returns((User) null);\n\t\t_postRepo.GetVotes(Arg.Any<int>()).Returns(Task.FromResult(new Dictionary<int, string>()));\n\t\tawait service.ToggleVoteReturnCountAndIsVoted(new Post { PostID = 123 }, new User { UserID = 456 }, \"\", \"\", \"\");\n\t\tawait _eventPub.DidNotReceive().ProcessEvent(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<string>(), false);\n\t}\n\n\t[Fact]\n\tpublic async Task ToggleVoteCallsEventPub()\n\t{\n\t\tvar service = GetService();\n\t\tvar voteUpUser = new User { UserID = 777 };\n\t\t_userService.GetUser(voteUpUser.UserID).Returns(Task.FromResult(voteUpUser));\n\t\t_postRepo.GetVotes(Arg.Any<int>()).Returns(Task.FromResult(new Dictionary<int, string>()));\n\t\tawait service.ToggleVoteReturnCountAndIsVoted(new Post { PostID = 123, UserID = voteUpUser.UserID }, new User { UserID = 456 }, \"\", \"\", \"\");\n\t\tawait _eventPub.Received().ProcessEvent(Arg.Any<string>(), voteUpUser, EventDefinitionService.StaticEventIDs.PostVote, false);\n\t}\n\n\t[Fact]\n\tpublic async Task ToggleVoteCallsNotification()\n\t{\n\t\tvar service = GetService();\n\t\tvar voteUpUser = new User { UserID = 777, Name = \"Diana\" };\n\t\tvar title = \"the title\";\n\t\t_userService.GetUser(voteUpUser.UserID).Returns(Task.FromResult(voteUpUser));\n\t\t_postRepo.GetVotes(Arg.Any<int>()).Returns(Task.FromResult(new Dictionary<int, string>()));\n\n\t\tawait service.ToggleVoteReturnCountAndIsVoted(new Post { PostID = 123, UserID = voteUpUser.UserID }, new User { UserID = 456, Name = \"Voter\" }, \"\", \"\", title);\n\n\t\tawait _notificationAdapter.Received().Vote(\"Voter\", title, 123, voteUpUser.UserID);\n\t}\n\n\t[Fact]\n\tpublic async Task ToggleVoteDoesNotCallWhenUserIsPoster()\n\t{\n\t\tvar service = GetService();\n\t\tvar voteUpUser = new User { UserID = 456 };\n\t\t_userService.GetUser(voteUpUser.UserID).Returns(Task.FromResult(voteUpUser));\n\t\t_postRepo.GetVotes(Arg.Any<int>()).Returns(Task.FromResult(new Dictionary<int, string>()));\n\t\tawait service.ToggleVoteReturnCountAndIsVoted(new Post { PostID = 123, UserID = voteUpUser.UserID }, new User { UserID = 456 }, \"\", \"\", \"\");\n\t\tawait _eventPub.DidNotReceive().ProcessEvent(Arg.Any<string>(), Arg.Any<User>(), EventDefinitionService.StaticEventIDs.PostVote, false);\n\t}\n\n\t[Fact]\n\tpublic async Task ToggleVotePassesPubStringWithFormattedStuff()\n\t{\n\t\tvar service = GetService();\n\t\tvar voteUpUser = new User { UserID = 777 };\n\t\tconst string userUrl = \"http://abc\";\n\t\tconst string topicUrl = \"http://def\";\n\t\tconst string title = \"blah blah blah\";\n\t\t_userService.GetUser(voteUpUser.UserID).Returns(Task.FromResult(voteUpUser));\n\t\t_postRepo.GetVotes(Arg.Any<int>()).Returns(Task.FromResult(new Dictionary<int, string>()));\n\t\tvar message = String.Empty;\n\t\tawait _eventPub.ProcessEvent(Arg.Do<string>(x => message = x), Arg.Any<User>(), EventDefinitionService.StaticEventIDs.PostVote, false);\n\t\tawait service.ToggleVoteReturnCountAndIsVoted(new Post { PostID = 123, UserID = voteUpUser.UserID }, new User { UserID = 456 }, userUrl, topicUrl, title);\n\t\tAssert.Contains(userUrl, message);\n\t\tAssert.Contains(topicUrl, message);\n\t\tAssert.Contains(title, message);\n\t}\n\n\t[Fact]\n\tpublic void GenerateParsedTextPreviewCallsForumCodeForPlainText()\n\t{\n\t\tvar service = GetService();\n\t\tconst string input = \"ohgorigh\";\n\t\tconst string output = \"90eyuw\";\n\t\t_textParsingService.ForumCodeToHtml(input).Returns(output);\n\t\tvar result = service.GenerateParsedTextPreview(input, true);\n\t\tAssert.Equal(output, result);\n\t\t_textParsingService.Received().ForumCodeToHtml(input);\n\t}\n\n\t[Fact]\n\tpublic void GenerateParsedTextPreviewCallsHtmlForRichText()\n\t{\n\t\tvar service = GetService();\n\t\tconst string input = \"ohgorigh\";\n\t\tconst string output = \"90eyuw\";\n\t\t_textParsingService.ClientHtmlToHtml(input).Returns(output);\n\t\tvar result = service.GenerateParsedTextPreview(input, false);\n\t\tAssert.Equal(output, result);\n\t\t_textParsingService.Received().ClientHtmlToHtml(input);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/PrivateMessageServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class PrivateMessageServiceTests\n{\n\tprivate PrivateMessageService GetService()\n\t{\n\t\t_mockPMRepo = Substitute.For<IPrivateMessageRepository>();\n\t\t_mockSettings = Substitute.For<ISettingsManager>();\n\t\t_mockTextParse = Substitute.For<ITextParsingService>();\n\t\t_mockBroker = Substitute.For<IBroker>();\n\t\tvar service = new PrivateMessageService(_mockPMRepo, _mockSettings, _mockTextParse, _mockBroker);\n\t\treturn service;\n\t}\n\n\tprivate IPrivateMessageRepository _mockPMRepo;\n\tprivate ISettingsManager _mockSettings;\n\tprivate ITextParsingService _mockTextParse;\n\tprivate IBroker _mockBroker;\n\n\t[Fact]\n\tpublic async Task CreateNullTextThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(() => service.Create(null, new User(), new List<User> { new User() }));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateEmptyTextThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(() => service.Create(String.Empty, new User(), new List<User> { new User() }));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateNullUserThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(() => service.Create(\"oho h\", null, new List<User> { new User() }));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateNullToUsersThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentException>(() => service.Create(\"oho h\", new User(), null));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateZeroToUsersThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentException>(() => service.Create(\"oho h\", new User(), new List<User>()));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateSerializedUser()\n\t{\n\t\tvar service = GetService();\n\t\tvar pm = await service.Create(\"oihefio\", new User { UserID = 12, Name = \"jeff\"}, new List<User> {new User { UserID = 45, Name = \"diana\"}});\n\t\tAssert.Equal(45, pm.Users[0].GetProperty(\"userID\").GetInt32());\n\t\tAssert.Equal(\"diana\", pm.Users[0].GetProperty(\"name\").GetString());\n\t}\n\n\t[Fact]\n\tpublic async Task CreateSerializedUsers()\n\t{\n\t\tvar service = GetService();\n\t\tvar pm = await service.Create(\"oihefio\", new User { UserID = 12, Name = \"jeff\" }, new List<User> { new User { UserID = 45, Name = \"diana\" }, new User { UserID = 78, Name = \"simon\" } });\n\t\tAssert.Equal(45, pm.Users[0].GetProperty(\"userID\").GetInt32());\n\t\tAssert.Equal(\"diana\", pm.Users[0].GetProperty(\"name\").GetString());\n\t\tAssert.Equal(78, pm.Users[1].GetProperty(\"userID\").GetInt32());\n\t\tAssert.Equal(\"simon\", pm.Users[1].GetProperty(\"name\").GetString());\n\t}\n\n\t[Fact]\n\tpublic async Task CreateCallsNotificationBroker()\n\t{\n\t\tvar service = GetService();\n\t\t_mockPMRepo.GetUnreadCount(45).Returns(Task.FromResult(3));\n\n\t\tvar pm = await service.Create(\"oihefio\", new User { UserID = 12, Name = \"jeff\" }, new List<User> { new User { UserID = 45, Name = \"diana\" } });\n\n\t\t_mockBroker.Received().NotifyPMCount(45, 3);\n\t}\n\n\t[Fact]\n\tpublic async Task CreatePMPersistedIDReturned()\n\t{\n\t\tvar service = GetService();\n\t\tvar persist = new PrivateMessage();\n\t\t_mockPMRepo.CreatePrivateMessage(Arg.Do<PrivateMessage>(x => persist = x)).Returns(Task.FromResult(69));\n\t\t_mockTextParse.EscapeHtmlAndCensor(\"ohqefwwf\").Returns(\"ohqefwwf\");\n\t\tvar pm = await service.Create(\"oihefio\", new User { UserID = 12, Name = \"jeff\" }, new List<User> { new User { UserID = 45, Name = \"diana\" }, new User { UserID = 67, Name = \"simon\"} });\n\t\tAssert.Equal(69, pm.PMID);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateAllUsersPresisted()\n\t{\n\t\tvar user = new User { UserID = 12 };\n\t\tvar to1 = new User { UserID = 45 };\n\t\tvar to2 = new User { UserID = 67 };\n\t\tvar service = GetService();\n\t\tvar users = new List<int>();\n\t\tvar originalUser = new List<int>();\n\t\t_mockPMRepo.CreatePrivateMessage(Arg.Any<PrivateMessage>()).Returns(Task.FromResult(69));\n\t\tawait _mockPMRepo.AddUsers(Arg.Any<int>(), Arg.Do<List<int>>(x => originalUser = x), Arg.Any<DateTime>(), false);\n\t\tawait _mockPMRepo.AddUsers(Arg.Any<int>(), Arg.Do<List<int>>(x => users = x), Arg.Any<DateTime>(), false);\n\n\t\tawait service.Create(\"oihefio\", user, new List<User> { to1, to2 });\n\n\t\tAssert.Equal(2, users.Count);\n\t\tAssert.Equal(to1.UserID, users[0]);\n\t\tAssert.Equal(to2.UserID, users[1]);\n\t\t// TODO: figure out multiple setups with same parameters\n\t\t//Assert.Equal(user.UserID, originalUser[0]);\n\t}\n\n\t[Fact]\n\tpublic async Task CreatePostPersist()\n\t{\n\t\tvar user = new User { UserID = 12, Name = \"jeff\" };\n\t\tvar to1 = new User { UserID = 45 };\n\t\tvar to2 = new User { UserID = 67 };\n\t\tvar service = GetService();\n\t\t_mockPMRepo.CreatePrivateMessage(Arg.Any<PrivateMessage>()).Returns(Task.FromResult(69));\n\t\tvar post = new PrivateMessagePost();\n\t\tawait _mockPMRepo.AddPost(Arg.Do<PrivateMessagePost>(x => post = x));\n\t\t_mockTextParse.ForumCodeToHtml(\"oihefio\").Returns(\"oihefio\");\n\t\tawait service.Create(\"oihefio\", user, new List<User> { to1, to2 });\n\t\tAssert.Equal(\"oihefio\", post.FullText);\n\t\tAssert.Equal(\"jeff\", post.Name);\n\t\tAssert.Equal(69, post.PMID);\n\t\tAssert.Equal(user.UserID, post.UserID);\n\t}\n\n\t[Fact]\n\tpublic async Task ReplyNullPMThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentException>(() => service.Reply(null, \"ohifwefhi\", new User()));\n\t}\n\n\t[Fact]\n\tpublic async Task ReplyNoIdPMThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentException>(() => service.Reply(new PrivateMessage(), \"ohifwefhi\", new User()));\n\t}\n\n\t[Fact]\n\tpublic async Task ReplyNullTextThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(() => service.Reply(new PrivateMessage{ PMID = 2 }, null, new User()));\n\t}\n\n\t[Fact]\n\tpublic async Task ReplyEmptyTextThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(() => service.Reply(new PrivateMessage { PMID = 2 }, String.Empty, new User()));\n\t}\n\n\t[Fact]\n\tpublic async Task ReplyNullUserThrows()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(() => service.Reply(new PrivateMessage { PMID = 2 }, \"wfwgrg\", null));\n\t}\n\n\t[Fact]\n\tpublic async Task ReplyMapsAndPresistsPost()\n\t{\n\t\tvar service = GetService();\n\t\tvar post = new PrivateMessagePost();\n\t\tawait _mockPMRepo.AddPost(Arg.Do<PrivateMessagePost>(x => post = x));\n\t\tvar user = new User { UserID = 1, Name = \"jeff\"};\n\t\tvar pm = new PrivateMessage {PMID = 2};\n\t\tvar text = \"mah message\";\n\t\t_mockTextParse.ForumCodeToHtml(text).Returns(text);\n\t\t_mockPMRepo.GetUsers(pm.PMID).Returns(Task.FromResult(new List<PrivateMessageUser> {new PrivateMessageUser {UserID = user.UserID}}));\n\t\t_mockPMRepo.GetUnreadCount(user.UserID).Returns(Task.FromResult(42));\n\n\t\tawait service.Reply(pm, text, user);\n\n\t\tAssert.Equal(text, post.FullText);\n\t\tAssert.Equal(user.Name, post.Name);\n\t\tAssert.Equal(user.UserID, post.UserID);\n\t\tAssert.Equal(pm.PMID, post.PMID);\n\t\t_mockBroker.Received().NotifyPMCount(user.UserID, 42);\n\t}\n\n\t[Fact]\n\tpublic async Task ReplyThrowsIfUserIsntOnPM()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 1 };\n\t\t_mockPMRepo.GetUsers(Arg.Any<int>()).Returns(Task.FromResult(new List<PrivateMessageUser> { new PrivateMessageUser { UserID = 456 } }));\n\t\tawait Assert.ThrowsAsync<Exception>(() => service.Reply(new PrivateMessage { PMID = 2 }, \"wohfwo\", user));\n\t}\n\n\t[Fact]\n\tpublic async Task IsUserInPMTrue()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar pm = new PrivateMessage { PMID = 2 };\n\t\t_mockPMRepo.GetUsers(pm.PMID).Returns(Task.FromResult(new List<PrivateMessageUser> { new PrivateMessageUser { UserID = user.UserID } }));\n\t\tAssert.True(await service.IsUserInPM(user.UserID, pm.PMID));\n\t}\n\n\t[Fact]\n\tpublic async Task IsUserInPMFalse()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar pm = new PrivateMessage { PMID = 2 };\n\t\t_mockPMRepo.GetUsers(pm.PMID).Returns(Task.FromResult(new List<PrivateMessageUser> { new PrivateMessageUser { UserID = 765 } }));\n\t\tAssert.False(await service.IsUserInPM(user.UserID, pm.PMID));\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/ProfileServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class ProfileServiceTests\n{\n\tprivate IProfileRepository _profileRepo;\n\tprivate ITextParsingService _textParsingService;\n\tprivate IPointLedgerRepository _pointLedger;\n\n\tprivate ProfileService GetService()\n\t{\n\t\t_profileRepo = Substitute.For<IProfileRepository>();\n\t\t_textParsingService = Substitute.For<ITextParsingService>();\n\t\t_pointLedger = Substitute.For<IPointLedgerRepository>();\n\t\treturn new ProfileService(_profileRepo, _textParsingService, _pointLedger);\n\t}\n\n\t[Fact]\n\tpublic async Task GetProfile()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile { UserID = 123, Location = \"Cleveland\" };\n\t\tvar user = UserServiceTests.GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(profile));\n\t\tvar result = await service.GetProfile(user);\n\t\tAssert.Equal(profile, result);\n\t\tawait _profileRepo.Received().GetProfile(user.UserID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetProfileReturnsNullForNullUser()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = await service.GetProfile(null);\n\t\tAssert.Null(result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetProfileForEditParsesSigRichText()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile { UserID = 123, Location = \"Cleveland\", Signature = \"blah\", IsPlainText = false };\n\t\tvar user = UserServiceTests.GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(profile));\n\t\t_textParsingService.HtmlToClientHtml(\"blah\").Returns(\"parsed\");\n\n\t\tvar result = await service.GetProfileForEdit(user);\n\n\t\tAssert.Equal(\"parsed\", result.Signature);\n\t}\n\n\t[Fact]\n\tpublic async Task GetProfileForEditParsesSigPlainText()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile { UserID = 123, Location = \"Cleveland\", Signature = \"blah\", IsPlainText = true };\n\t\tvar user = UserServiceTests.GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(profile));\n\t\t_textParsingService.HtmlToForumCode(\"blah\").Returns(\"parsed\");\n\n\t\tvar result = await service.GetProfileForEdit(user);\n\n\t\tAssert.Equal(\"parsed\", result.Signature);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserProfilePlainText()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar returnedProfile = new Profile { UserID = 1, IsPlainText = true };\n\t\tvar profile = new Profile();\n\t\t_profileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tawait _profileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\t_textParsingService.ForumCodeToHtml(Arg.Any<string>()).Returns(\"parsed\");\n\t\tvar userEdit = new UserEditProfile\n\t\t{\n\t\t\tDob = new DateTime(2000, 1, 1),\n\t\t\tHideVanity = true,\n\t\t\tInstagram = \"i\",\n\t\t\tIsPlainText = true,\n\t\t\tIsSubscribed = true,\n\t\t\tLocation = \"l\",\n\t\t\tFacebook = \"fb\",\n\t\t\tShowDetails = true,\n\t\t\tSignature = \"s\",\n\t\t\tWeb = \"w\",\n\t\t\tIsAutoFollowOnReply = true\n\t\t};\n\n\t\tawait service.EditUserProfile(user, userEdit);\n\n\t\tawait _profileRepo.Received().Update(Arg.Any<Profile>());\n\t\tAssert.Equal(new DateTime(2000, 1, 1), profile.Dob);\n\t\tAssert.True(profile.HideVanity);\n\t\tAssert.Equal(\"i\", profile.Instagram);\n\t\tAssert.True(profile.IsPlainText);\n\t\tAssert.True(profile.IsSubscribed);\n\t\tAssert.True(profile.IsAutoFollowOnReply);\n\t\tAssert.Equal(\"l\", profile.Location);\n\t\tAssert.Equal(\"fb\", profile.Facebook);\n\t\tAssert.True(profile.ShowDetails);\n\t\tAssert.Equal(\"parsed\", profile.Signature);\n\t\tAssert.Equal(\"w\", profile.Web);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserProfileRichText()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar returnedProfile = new Profile { UserID = 1, IsPlainText = false };\n\t\tvar profile = new Profile();\n\t\t_profileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tawait _profileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\t_textParsingService.ClientHtmlToHtml(Arg.Any<string>()).Returns(\"parsed\");\n\t\tvar userEdit = new UserEditProfile\n\t\t{\n\t\t\tDob = new DateTime(2000, 1, 1),\n\t\t\tHideVanity = true,\n\t\t\tInstagram = \"i\",\n\t\t\tIsPlainText = true,\n\t\t\tIsSubscribed = true,\n\t\t\tLocation = \"l\",\n\t\t\tFacebook = \"fb\",\n\t\t\tShowDetails = true,\n\t\t\tSignature = \"s\",\n\t\t\tWeb = \"w\"\n\t\t};\n\n\t\tawait service.EditUserProfile(user, userEdit);\n\n\t\tAssert.Equal(\"parsed\", profile.Signature);\n\t}\n\n\t[Fact]\n\tpublic async Task GetProfileForEditParsesSigGuardForNull()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile { UserID = 123, Location = \"Cleveland\", Signature = null };\n\t\tvar user = UserServiceTests.GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\t_profileRepo.GetProfile(user.UserID).Returns(Task.FromResult(profile));\n\n\t\tvar result = await service.GetProfileForEdit(user);\n\n\t\t_textParsingService.DidNotReceive().ClientHtmlToForumCode(Arg.Any<string>());\n\t\tAssert.Equal(string.Empty, result.Signature);\n\t\tawait _profileRepo.Received().GetProfile(user.UserID);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateFromProfileObject()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile { UserID = 123, Location = \"Cleveland\" };\n\t\tawait service.Create(profile);\n\t\tawait _profileRepo.Received().Create(profile);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateFromProfileThrowsWithoutUserID()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile();\n\t\tawait Assert.ThrowsAsync<Exception>(() => service.Create(profile));\n\t\tawait _profileRepo.DidNotReceive().Create(profile);\n\t}\n\n\t[Fact]\n\tpublic async Task Update()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile { UserID = 123, Location = \"Cleveland\", Signature = \"\"};\n\t\t_profileRepo.Update(profile).Returns(Task.FromResult(true));\n\t\tawait service.Update(profile);\n\t\tawait _profileRepo.Received().Update(profile);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTrimsSig()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile { UserID = 123, Location = \"Cleveland\", Signature = \" \" };\n\t\tvar trimProfile = new Profile { Signature = \"no\"};\n\t\t_profileRepo.Update(Arg.Do<Profile>(x => trimProfile = x)).Returns(Task.FromResult(true));\n\t\tawait service.Update(profile);\n\t\tAssert.Equal(\"\", trimProfile.Signature);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateThrowsWithNoProfile()\n\t{\n\t\tvar service = GetService();\n\t\tvar profile = new Profile { UserID = 123, Location = \"Cleveland\", Signature = \"\" };\n\t\t_profileRepo.Update(profile).Returns(Task.FromResult(false));\n\t\tawait Assert.ThrowsAsync<Exception>(() => service.Update(profile));\n\t\tawait _profileRepo.Received().Update(profile);\n\t}\n\n\t[Fact]\n\tpublic async Task GetSigsOnlyTakesPostsWithShowSig()\n\t{\n\t\tvar posts = new List<Post>\n\t\t{\n\t\t\tnew Post { UserID = 1, ShowSig = false },\n\t\t\tnew Post { UserID = 2, ShowSig = true },\n\t\t\tnew Post { UserID = 3, ShowSig = false },\n\t\t\tnew Post { UserID = 4, ShowSig = true },\n\t\t\tnew Post { UserID = 5, ShowSig = true },\n\t\t\tnew Post { UserID = 6, ShowSig = false },\n\t\t};\n\t\tvar service = GetService();\n\t\tvar ids = new List<int>();\n\t\tawait _profileRepo.GetSignatures(Arg.Do<List<int>>(x => ids = x));\n\t\tawait service.GetSignatures(posts);\n\t\tAssert.Equal(3, ids.Count);\n\t\tAssert.Equal(2, ids[0]);\n\t\tAssert.Equal(4, ids[1]);\n\t\tAssert.Equal(5, ids[2]);\n\t}\n\n\t[Fact]\n\tpublic async Task GetSigsDoesntSendDupeUserIDs()\n\t{\n\t\tvar posts = new List<Post>\n\t\t{\n\t\t\tnew Post { UserID = 1, ShowSig = false },\n\t\t\tnew Post { UserID = 2, ShowSig = true },\n\t\t\tnew Post { UserID = 2, ShowSig = false },\n\t\t\tnew Post { UserID = 2, ShowSig = true },\n\t\t\tnew Post { UserID = 3, ShowSig = true },\n\t\t\tnew Post { UserID = 3, ShowSig = true },\n\t\t};\n\t\tvar service = GetService();\n\t\tvar ids = new List<int>();\n\t\tawait _profileRepo.GetSignatures(Arg.Do<List<int>>(x => ids = x));\n\t\tawait service.GetSignatures(posts);\n\t\tAssert.Equal(2, ids.Count);\n\t\tAssert.Equal(2, ids[0]);\n\t\tAssert.Equal(3, ids[1]);\n\t}\n\n\t[Fact]\n\tpublic async Task GetAvatarsDoesntSendDupeUserIDs()\n\t{\n\t\tvar posts = new List<Post>\n\t\t{\n\t\t\tnew Post { UserID = 1 },\n\t\t\tnew Post { UserID = 2 },\n\t\t\tnew Post { UserID = 2 },\n\t\t\tnew Post { UserID = 2 },\n\t\t\tnew Post { UserID = 3 },\n\t\t\tnew Post { UserID = 3 },\n\t\t};\n\t\tvar service = GetService();\n\t\tvar ids = new List<int>();\n\t\tawait _profileRepo.GetAvatars(Arg.Do<List<int>>(x => ids = x));\n\t\tawait service.GetAvatars(posts);\n\t\tAssert.Equal(3, ids.Count);\n\t\tAssert.Equal(1, ids[0]);\n\t\tAssert.Equal(2, ids[1]);\n\t\tAssert.Equal(3, ids[2]);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdatePointsUpdatesPoints()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tconst int total = 87;\n\t\t_pointLedger.GetPointTotal(user.UserID).Returns(Task.FromResult(total));\n\t\tawait service.UpdatePointTotal(user);\n\t\tawait _profileRepo.Received().UpdatePoints(user.UserID, total);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/QueuedEmailServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class QueuedEmailServiceTests\n{\n\tprivate IQueuedEmailMessageRepository _queuedEmailMessageRepo;\n\tprivate IEmailQueueRepository _emailQueueRepo;\n\tprivate ITenantService _tenantService;\n\n\tprivate QueuedEmailService GetService()\n\t{\n\t\t_queuedEmailMessageRepo = Substitute.For<IQueuedEmailMessageRepository>();\n\t\t_emailQueueRepo = Substitute.For<IEmailQueueRepository>();\n\t\t_tenantService = Substitute.For<ITenantService>();\n\t\treturn new QueuedEmailService(_queuedEmailMessageRepo, _emailQueueRepo, _tenantService);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateAndQueueEmailCallsRepoWithMessage()\n\t{\n\t\tvar service = GetService();\n\t\tvar message = new QueuedEmailMessage();\n\t\t_queuedEmailMessageRepo.CreateMessage(message).Returns(Task.FromResult(1));\n\t\t_tenantService.GetTenant().Returns(\"\");\n\n\t\tawait service.CreateAndQueueEmail(message);\n\n\t\tawait _queuedEmailMessageRepo.Received().CreateMessage(message);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateAndQueueEmailCallsEmailQueueWithCorrectPayload()\n\t{\n\t\tvar service = GetService();\n\t\tvar messageID = 123;\n\t\tvar message = new QueuedEmailMessage();\n\t\t_queuedEmailMessageRepo.CreateMessage(message).Returns(Task.FromResult(messageID));\n\t\tvar tenantID = \"t1\";\n\t\t_tenantService.GetTenant().Returns(tenantID);\n\t\tvar payload = new EmailQueuePayload();\n\t\tawait _emailQueueRepo.Enqueue(Arg.Do<EmailQueuePayload>(x => payload = x));\n\n\t\tawait service.CreateAndQueueEmail(message);\n\n\t\tAssert.Equal(messageID, payload.MessageID);\n\t\tAssert.Equal(EmailQueuePayloadType.FullMessage, payload.EmailQueuePayloadType);\n\t\tAssert.Equal(tenantID, payload.TenantID);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/SearchIndexWorkerTests.cs",
    "content": "using NSubstitute.ExceptionExtensions;\nusing NSubstitute.ReturnsExtensions;\n\nnamespace PopForums.Test.Services;\n\npublic class SearchIndexWorkerTests\n{\n\tprivate IErrorLog _errorLog;\n\tprivate ISearchIndexSubsystem _searchIndexSubsystem;\n\tprivate ISearchService _searchService;\n\t\n\tprivate ISearchIndexWorker GetWorker()\n\t{\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\t_searchIndexSubsystem = Substitute.For<ISearchIndexSubsystem>();\n\t\t_searchService = Substitute.For<ISearchService>();\n\t\treturn new SearchIndexWorker(_errorLog, _searchIndexSubsystem, _searchService);\n\t}\n\t\n\t[Fact]\n\tpublic void DoNothingWhenNoPayload()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_searchService.GetNextTopicForIndexing().ReturnsNull();\n\t\t\n\t\tworker.Execute();\n\n\t\t_searchIndexSubsystem.DidNotReceiveWithAnyArgs().DoIndex(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<bool>());\n\t\t_errorLog.DidNotReceiveWithAnyArgs().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\t\n\t[Fact]\n\tpublic void CallSearchIndexSubsystemWhenPayload()\n\t{\n\t\tvar worker = GetWorker();\n\t\tvar payload = new SearchIndexPayload { TopicID = 123, TenantID = \"tenant\", IsForRemoval = true };\n\t\t_searchService.GetNextTopicForIndexing().Returns(payload);\n\t\t\n\t\tworker.Execute();\n\n\t\t_searchIndexSubsystem.Received().DoIndex(123, \"tenant\", true);\n\t\t_errorLog.DidNotReceiveWithAnyArgs().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\n\t[Fact]\n\tpublic void LogWhenGetNextTopicForIndexingThrows()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_searchService.GetNextTopicForIndexing().ThrowsAsync(new Exception());\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_searchIndexSubsystem.DidNotReceiveWithAnyArgs().DoIndex(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<bool>());\n\t\t_errorLog.Received().Log(Arg.Any<Exception>(), ErrorSeverity.Error);\n\t}\n\t\n\t[Fact]\n\tpublic void LogWhenDoIndexThrows()\n\t{\n\t\tvar worker = GetWorker();\n\t\tvar payload = new SearchIndexPayload { TopicID = 123, TenantID = \"tenant\", IsForRemoval = true };\n\t\t_searchService.GetNextTopicForIndexing().Returns(payload);\n\t\t_searchIndexSubsystem.When(x => x.DoIndex(123, \"tenant\", true)).Throw(new Exception());\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_searchIndexSubsystem.Received().DoIndex(123, \"tenant\", true);\n\t\t_errorLog.Received().Log(Arg.Any<Exception>(), ErrorSeverity.Error);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/SearchServiceTests.cs",
    "content": "namespace PopForums.Test.Services;\n\npublic class SearchServiceTests\n{\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate ISearchRepository _mockSearchRepo;\n\tprivate IForumService _mockForumService;\n\tprivate ISearchIndexQueueRepository _searchIndexQueueRepo;\n\tprivate IErrorLog _errorLog;\n\n\tprivate SearchService GetService()\n\t{\n\t\t_mockSearchRepo = Substitute.For<ISearchRepository>();\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_mockForumService = Substitute.For<IForumService>();\n\t\t_searchIndexQueueRepo = Substitute.For<ISearchIndexQueueRepository>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\treturn new SearchService(_mockSearchRepo, _mockSettingsManager, _mockForumService, _searchIndexQueueRepo, _errorLog);\n\t}\n\n\t[Fact]\n\tpublic async Task GetJunkWords()\n\t{\n\t\tvar words = new List<string>();\n\t\tvar service = GetService();\n\t\t_mockSearchRepo.GetJunkWords().Returns(Task.FromResult(words));\n\t\tvar result = await service.GetJunkWords();\n\t\tawait _mockSearchRepo.Received().GetJunkWords();\n\t\tAssert.Same(words, result);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateWord()\n\t{\n\t\tvar service = GetService();\n\t\tawait service.CreateJunkWord(\"blah\");\n\t\tawait _mockSearchRepo.Received().CreateJunkWord(\"blah\");\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteWord()\n\t{\n\t\tvar service = GetService();\n\t\tawait service.DeleteJunkWord(\"blah\");\n\t\tawait _mockSearchRepo.Received().DeleteJunkWord(\"blah\");\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsReturnsValidResponseWithNoResultsWhenSearchTermIsNull()\n\t{\n\t\tvar service = GetService();\n\t\t_mockForumService.GetNonViewableForumIDs(null).Returns(Task.FromResult(new List<int>()));\n\t\t_mockSettingsManager.Current.TopicsPerPage.Returns(20);\n\n\t\tvar result = await service.GetTopics(null, SearchType.Rank, null, false, 1);\n\n\t\tAssert.Empty(result.Item1.Data);\n\t\tAssert.True(result.Item1.IsValid);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsReturnsValidResponseWithNoResultsWhenSearchTermIsEmpty()\n\t{\n\t\tvar service = GetService();\n\t\t_mockForumService.GetNonViewableForumIDs(null).Returns(Task.FromResult(new List<int>()));\n\t\t_mockSettingsManager.Current.TopicsPerPage.Returns(20);\n\n\t\tvar result = await service.GetTopics(String.Empty, SearchType.Rank, null, false, 1);\n\n\t\tAssert.Empty(result.Item1.Data);\n\t\tAssert.True(result.Item1.IsValid);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsIsCalledWithTheRightParameters()\n\t{\n\t\tvar query = \"test\";\n\t\tvar user = new User();\n\t\tvar noViewIDs = new List<int> {1};\n\t\tvar service = GetService();\n\t\t_mockForumService.GetNonViewableForumIDs(user).Returns(Task.FromResult(noViewIDs));\n\t\t_mockSettingsManager.Current.TopicsPerPage.Returns(20);\n\t\t_mockSearchRepo.SearchTopics(query, noViewIDs, SearchType.Rank, 1, 20).Returns((Tuple.Create(new Response<List<Topic>>(new List<Topic>()), 0)));\n\n\t\tawait service.GetTopics(query, SearchType.Rank, user, false, 1);\n\n\t\tawait _mockSearchRepo.Received().SearchTopics(query, noViewIDs, SearchType.Rank, 1, 20);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsOutsCorrectPagerContextAndValidResult()\n\t{\n\t\tvar query = \"test\";\n\t\tvar user = new User();\n\t\tvar noViewIDs = new List<int> { 1 };\n\t\tvar list = new List<Topic>();\n\t\tvar service = GetService();\n\t\t_mockForumService.GetNonViewableForumIDs(user).Returns(Task.FromResult(noViewIDs));\n\t\t_mockSettingsManager.Current.TopicsPerPage.Returns(20);\n\t\tvar count = 50;\n\t\t_mockSearchRepo.SearchTopics(query, noViewIDs, SearchType.Rank, 21, 20).Returns(Tuple.Create(new Response<List<Topic>>(list), count));\n\n\t\tvar (response, pagerContext) = await service.GetTopics(query, SearchType.Rank, user, false, 2);\n\n\t\tAssert.Equal(20, pagerContext.PageSize);\n\t\tAssert.Equal(2, pagerContext.PageIndex);\n\t\tAssert.Equal(3, pagerContext.PageCount);\n\t\tAssert.True(response.IsValid);\n\t\tAssert.Same(list, response.Data);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsReturnsEmptyResultIsValidFalseAndAnemicPagerContext()\n\t{\n\t\tvar query = \"test\";\n\t\tvar user = new User();\n\t\tvar noViewIDs = new List<int> { 1 };\n\t\tvar service = GetService();\n\t\t_mockForumService.GetNonViewableForumIDs(user).Returns(Task.FromResult(noViewIDs));\n\t\t_mockSettingsManager.Current.TopicsPerPage.Returns(20);\n\t\tvar count = 50;\n\t\t_mockSearchRepo.SearchTopics(query, noViewIDs, SearchType.Rank, 21, 20).Returns(Tuple.Create(new Response<List<Topic>>(new List<Topic>(), false), count));\n\n\t\tvar (response, pagerContext) = await service.GetTopics(query, SearchType.Rank, user, false, 2);\n\n\t\tAssert.Empty(response.Data);\n\t\tAssert.False(response.IsValid);\n\t\tAssert.Equal(1, pagerContext.PageSize);\n\t\tAssert.Equal(1, pagerContext.PageIndex);\n\t\tAssert.Equal(1, pagerContext.PageCount);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/SecurityLogServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class SecurityLogServiceTests\n{\n\tprivate ISecurityLogRepository _securityLogRepo;\n\tprivate IUserRepository _userRepo;\n\n\tprivate SecurityLogService GetService()\n\t{\n\t\t_securityLogRepo = Substitute.For<ISecurityLogRepository>();\n\t\t_userRepo = Substitute.For<IUserRepository>();\n\t\treturn new SecurityLogService(_securityLogRepo, _userRepo);\n\t}\n\n\t[Fact]\n\tpublic async Task GetEntriesByUserID()\n\t{\n\t\tvar service = GetService();\n\t\tconst int id = 123;\n\t\tawait service.GetLogEntriesByUserID(id, DateTime.MinValue, DateTime.MaxValue);\n\t\tawait _securityLogRepo.Received().GetByUserID(id, DateTime.MinValue, DateTime.MaxValue);\n\t}\n\n\t[Fact]\n\tpublic async Task GetEntriesByUserName()\n\t{\n\t\tvar service = GetService();\n\t\tconst int id = 123;\n\t\tconst string name = \"jeff\";\n\t\t_userRepo.GetUserByName(name).Returns(Task.FromResult(new User { UserID = id, Name = name}));\n\t\tawait service.GetLogEntriesByUserName(name, DateTime.MinValue, DateTime.MaxValue);\n\t\tawait _securityLogRepo.Received().GetByUserID(id, DateTime.MinValue, DateTime.MaxValue);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateNullIp()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(() => service.CreateLogEntry(new User(), new User(), null, \"\", SecurityLogType.Undefined));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateNullMessage()\n\t{\n\t\tvar service = GetService();\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(() => service.CreateLogEntry(new User(), new User(), \"\", null, SecurityLogType.Undefined));\n\t}\n\n\t[Fact]\n\tpublic async Task Create()\n\t{\n\t\tvar service = GetService();\n\t\tSecurityLogEntry entry = null;\n\t\tawait _securityLogRepo.Create(Arg.Do<SecurityLogEntry>(x => entry = x));\n\t\tawait service.CreateLogEntry(new User { UserID = 1 }, new User { UserID = 2 }, \"123\", \"msg\", SecurityLogType.Undefined);\n\t\tAssert.Equal(1, entry.UserID);\n\t\tAssert.Equal(2, entry.TargetUserID);\n\t\tAssert.Equal(\"123\", entry.IP);\n\t\tAssert.Equal(\"msg\", entry.Message);\n\t\tAssert.Equal(SecurityLogType.Undefined, entry.SecurityLogType);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/SetupServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class SetupServiceTests\n{\n\tprivate ISetupRepository _setupRepository;\n\tprivate IUserService _userService;\n\tprivate ISettingsManager _settingsManager;\n\tprivate IProfileService _profileService;\n\n\tprivate SetupService GetService()\n\t{\n\t\t// kind of gross leaky abstraction here\n\t\tvar staticSetupIndicator = typeof(SetupService).GetField(\"_isConnectionSetupGood\", BindingFlags.Static | BindingFlags.NonPublic);\n\t\tstaticSetupIndicator.SetValue(null, null);\n\t\t_setupRepository = Substitute.For<ISetupRepository>();\n\t\t_userService = Substitute.For<IUserService>();\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_profileService = Substitute.For<IProfileService>();\n\t\treturn new SetupService(_setupRepository, _userService, _settingsManager,\n\t\t\t_profileService);\n\t}\n\n\tpublic class IsRuntimeConnectionAndSetupGoodTests : SetupServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic void GoodConnectionAndSetupAlwaysReturnsTrue()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_setupRepository.IsConnectionPossible().Returns(true);\n\t\t\t_setupRepository.IsDatabaseSetup().Returns(true);\n\n\t\t\tvar result1 = service.IsRuntimeConnectionAndSetupGood();\n\t\t\tvar result2 = service.IsRuntimeConnectionAndSetupGood();\n\n\t\t\tAssert.True(result1);\n\t\t\tAssert.True(result2);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void GoodConnectionAndSetupOnlyCallsReposOnce()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_setupRepository.IsConnectionPossible().Returns(true);\n\t\t\t_setupRepository.IsDatabaseSetup().Returns(true);\n\n\t\t\tservice.IsRuntimeConnectionAndSetupGood();\n\t\t\tservice.IsRuntimeConnectionAndSetupGood();\n\t\t\tservice.IsRuntimeConnectionAndSetupGood();\n\t\t\tservice.IsRuntimeConnectionAndSetupGood();\n\t\t\tservice.IsRuntimeConnectionAndSetupGood();\n\n\t\t\t_setupRepository.Received().IsDatabaseSetup();\n\t\t\t_setupRepository.Received().IsConnectionPossible();\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void BadConnectionReturnsFalse()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_setupRepository.IsConnectionPossible().Returns(false);\n\n\t\t\tvar result = service.IsRuntimeConnectionAndSetupGood();\n\n\t\t\tAssert.False(result);\n\t\t}\n\n\t\t[Fact]\n\t\tpublic void GoodConnectionButNotSetupReturnsFalse()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\t_setupRepository.IsConnectionPossible().Returns(true);\n\t\t\t_setupRepository.IsDatabaseSetup().Returns(false);\n\n\t\t\tvar result = service.IsRuntimeConnectionAndSetupGood();\n\n\t\t\tAssert.False(result);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/SitemapServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class SitemapServiceTests\n{\n\tprivate SitemapService GetService()\n\t{\n\t\t_topicRepo = Substitute.For<ITopicRepository>();\n\t\t_forumRepo = Substitute.For<IForumRepository>();\n\t\treturn new SitemapService(_topicRepo, _forumRepo);\n\t}\n\n\tprivate ITopicRepository _topicRepo;\n\tprivate IForumRepository _forumRepo;\n\n\tpublic class GetSitemapPageCount : SitemapServiceTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task ZeroTopicsReturns1()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar list = new Dictionary<int, List<string>>();\n\t\t\t_forumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(list));\n\t\t\t_topicRepo.GetTopicCount(false, Arg.Any<List<int>>()).Returns(Task.FromResult(0));\n\n\t\t\tvar pageCount = await service.GetSitemapPageCount();\n\n\t\t\tAssert.Equal(1, pageCount);\n\t\t}\n\t\t\t\n\t\t[Fact]\n\t\tpublic async Task MaxTopicsReturns1()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar list = new Dictionary<int, List<string>>();\n\t\t\t_forumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(list));\n\t\t\t_topicRepo.GetTopicCount(false, Arg.Any<List<int>>()).Returns(Task.FromResult(30000));\n\n\t\t\tvar pageCount = await service.GetSitemapPageCount();\n\n\t\t\tAssert.Equal(1, pageCount);\n\t\t}\n\t\t\t\n\t\t[Fact]\n\t\tpublic async Task MaxPlusOneTopicsReturns2()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar list = new Dictionary<int, List<string>>();\n\t\t\t_forumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(list));\n\t\t\t_topicRepo.GetTopicCount(false, Arg.Any<List<int>>()).Returns(Task.FromResult(30001));\n\n\t\t\tvar pageCount = await service.GetSitemapPageCount();\n\n\t\t\tAssert.Equal(2, pageCount);\n\t\t}\n\t\t\t\n\t\t[Fact]\n\t\tpublic async Task NonViewableListPassedToTopicRepoForCount()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar list = new Dictionary<int, List<string>>\n\t\t\t{\n\t\t\t\t{1, new List<string>()},\n\t\t\t\t{2, new List<string>{\"Admin\"}},\n\t\t\t\t{3, new List<string>{\"Admin\",\"Moderator\"}},\n\t\t\t\t{4, new List<string>()}\n\t\t\t};\n\t\t\t_forumRepo.GetForumViewRestrictionRoleGraph().Returns(Task.FromResult(list));\n\t\t\tvar returnList = new List<int>();\n\t\t\t_topicRepo.GetTopicCount(false, Arg.Do<List<int>>(x => returnList = x)).Returns(Task.FromResult(30001));\n\n\t\t\tawait service.GetSitemapPageCount();\n\n\t\t\tAssert.Equal(2, returnList.Count);\n\t\t\tAssert.Equal(2, returnList[0]);\n\t\t\tAssert.Equal(3, returnList[1]);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/SubscribeNotificationWorkerTests.cs",
    "content": "using NSubstitute.ReturnsExtensions;\n\nnamespace PopForums.Test.Services;\n\npublic class SubscribeNotificationWorkerTests\n{\n\tprivate ISubscribeNotificationRepository _subscribeNotificationRepository;\n\tprivate ISubscribedTopicsService _subscribedTopicsService;\n\tprivate INotificationAdapter _notificationAdapter;\n\tprivate IErrorLog _errorLog;\n\n\tprivate SubscribeNotificationWorker GetWorker()\n\t{\n\t\t_subscribeNotificationRepository = Substitute.For<ISubscribeNotificationRepository>();\n\t\t_subscribedTopicsService = Substitute.For<ISubscribedTopicsService>();\n\t\t_notificationAdapter = Substitute.For<INotificationAdapter>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\treturn new SubscribeNotificationWorker(_subscribeNotificationRepository, _subscribedTopicsService, _notificationAdapter, _errorLog);\n\t}\n\n\t[Fact]\n\tpublic void NoPaylodNoOtherCalls()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_subscribeNotificationRepository.Dequeue().ReturnsNull();\n\t\t\n\t\tworker.Execute();\n\n\t\t_subscribedTopicsService.DidNotReceiveWithAnyArgs().GetSubscribedUserIDs(Arg.Any<int>());\n\t\t_notificationAdapter.DidNotReceiveWithAnyArgs().Reply(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(),\n\t\t\tArg.Any<int>(), Arg.Any<string>());\n\t\t_errorLog.DidNotReceiveWithAnyArgs().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\n\t[Fact]\n\tpublic void PayloadValuesCallNotificationAdapter()\n\t{\n\t\tvar worker = GetWorker();\n\t\tvar payload = new SubscribeNotificationPayload { TopicID = 1, PostingUserName = \"Diana\", PostingUserID = 7, TopicTitle = \"blah\", TenantID = \"pf\"};\n\t\tvar userIDs = new List<int> { 2, 3 };\n\t\t_subscribeNotificationRepository.Dequeue().Returns(payload);\n\t\t_subscribedTopicsService.GetSubscribedUserIDs(payload.TopicID).Returns(userIDs);\n\t\t\n\t\tworker.Execute();\n\n\t\t_notificationAdapter.Received().Reply(payload.PostingUserName, payload.TopicTitle, payload.TopicID, userIDs[0],\n\t\t\tpayload.TenantID);\n\t\t_notificationAdapter.Received().Reply(payload.PostingUserName, payload.TopicTitle, payload.TopicID, userIDs[1],\n\t\t\tpayload.TenantID);\n\t\t_errorLog.DidNotReceiveWithAnyArgs().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/SubscribedTopicsServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class SubscribedTopicsServiceTests\n{\n\tprivate ISubscribedTopicsRepository _mockSubRepo;\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate INotificationAdapter _mockNotificationAdapter;\n\tprivate ISubscribeNotificationRepository _subNotificationRepo;\n\n\tprivate SubscribedTopicsService GetService()\n\t{\n\t\t_mockSubRepo = Substitute.For<ISubscribedTopicsRepository>();\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_mockNotificationAdapter = Substitute.For<INotificationAdapter>();\n\t\t_subNotificationRepo = Substitute.For<ISubscribeNotificationRepository>();\n\t\treturn new SubscribedTopicsService(_mockSubRepo, _mockSettingsManager, _mockNotificationAdapter, _subNotificationRepo);\n\t}\n\n\t[Fact]\n\tpublic async Task AddSubTopic()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456 };\n\t\t_mockSubRepo.IsTopicSubscribed(user.UserID, topic.TopicID).Returns(Task.FromResult(false));\n\n\t\tawait service.AddSubscribedTopic(user.UserID, topic.TopicID);\n\n\t\tawait _mockSubRepo.Received().AddSubscribedTopic(user.UserID, topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task DoNotAddSubTopicIfAlreadySub()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456 };\n\t\t_mockSubRepo.IsTopicSubscribed(user.UserID, topic.TopicID).Returns(Task.FromResult(true));\n\n\t\tawait service.AddSubscribedTopic(user.UserID, topic.TopicID);\n\n\t\tawait _mockSubRepo.DidNotReceive().AddSubscribedTopic(user.UserID, topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task RemoveSubTopic()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456 };\n\t\tawait service.RemoveSubscribedTopic(user, topic);\n\t\tawait _mockSubRepo.Received().RemoveSubscribedTopic(user.UserID, topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task TryRemoveSubTopic()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456 };\n\t\tawait service.TryRemoveSubscribedTopic(user, topic);\n\t\tawait _mockSubRepo.Received().RemoveSubscribedTopic(user.UserID, topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task TryRemoveSubTopicNullTopic()\n\t{\n\t\tvar service = GetService();\n\t\tvar user = new User { UserID = 123 };\n\t\tawait service.TryRemoveSubscribedTopic(user, null);\n\t\tawait _mockSubRepo.DidNotReceive().RemoveSubscribedTopic(Arg.Any<int>(), Arg.Any<int>());\n\t}\n\n\t[Fact]\n\tpublic async Task TryRemoveSubTopicNullUser()\n\t{\n\t\tvar service = GetService();\n\t\tvar topic = new Topic { TopicID = 456 };\n\t\tawait service.TryRemoveSubscribedTopic(null, topic);\n\t\tawait _mockSubRepo.DidNotReceive().RemoveSubscribedTopic(Arg.Any<int>(), Arg.Any<int>());\n\t}\n\t\t\n\t[Fact]\n\tpublic async Task GetTopicsFromRepo()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tvar settings = new Settings { TopicsPerPage = 20 };\n\t\t_mockSettingsManager.Current.Returns(settings);\n\t\tvar list = new List<Topic>();\n\t\t_mockSubRepo.GetSubscribedTopics(user.UserID, 1, 20).Returns(Task.FromResult(list));\n\t\tvar (result, _) = await service.GetTopics(user, 1);\n\t\tAssert.Same(list, result);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsStartRowCalcd()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tvar settings = new Settings { TopicsPerPage = 20 };\n\t\t_mockSettingsManager.Current.Returns(settings);\n\t\tvar (_, pagerContext) = await service.GetTopics(user, 3);\n\t\tawait _mockSubRepo.Received().GetSubscribedTopics(user.UserID, 41, 20);\n\t\tAssert.Equal(20, pagerContext.PageSize);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/TextParsingServiceCleanForumCodeTests.cs",
    "content": "namespace PopForums.Test.Services;\n\npublic class TextParsingServiceCleanForumCodeTests\n{\n\tprivate TextParsingService GetService()\n\t{\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_settings = new Settings();\n\t\t_mockSettingsManager.Current.Returns(_settings);\n\t\treturn new TextParsingService(_mockSettingsManager);\n\t}\n\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate Settings _settings;\n\n\t[Fact]\n\tpublic void FilterDupeLineBreaks()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ahoihfohfo\\r\\noishfoihg\\r\\n\\r\\n\\r\\nehufhffh \\r\\n\\r\\n\\r\\n\\r\\nbbb\");\n\t\tAssert.Equal(\"ahoihfohfo\\r\\noishfoihg\\r\\n\\r\\nehufhffh \\r\\n\\r\\nbbb\", result);\n\t}\n\n\t[Fact]\n\tpublic void LeaveNormalLineBreaks()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"first\\r\\n\\r\\nsecond\");\n\t\tAssert.Equal(\"first\\r\\n\\r\\nsecond\", result);\n\t}\n\n\t[Fact]\n\tpublic void ConvertLonelyCarriageReturn()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"first\\nsecond\\r\\nthird\");\n\t\tAssert.Equal(\"first\\r\\nsecond\\r\\nthird\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveImageTagIfImagesDisallowed()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = false;\n\t\tvar result = service.CleanForumCode(\"fff[i]blah[/i] f f8whef 98wy 8wyef [image=blah.jpg]asfd affs[i]blah[/i]\");\n\t\tAssert.Equal(\"fff[i]blah[/i] f f8whef 98wy 8wyef asfd affs[i]blah[/i]\", result);\n\t}\n\n\t[Fact]\n\tpublic void AllowWellFormedImageTag()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"fff[i]blah[/i] f f8whef [image=\\\"blah.jpg\\\"] 98wy 8wyef [image=blah.jpg]asfd affs[i]blah[/i]\");\n\t\tAssert.Equal(\"fff[i]blah[/i] f f8whef [image=\\\"blah.jpg\\\"] 98wy 8wyef [image=blah.jpg]asfd affs[i]blah[/i]\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveMalFormedImageTag()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"fff[i]blah[/i] f f8whef [image \\\"blah.jpg\\\"] 98wy 8wyef [image=blah.jpg]asfd [image=\\\"blah.jpg\\\"]affs[i]blah[/i]\");\n\t\tAssert.Equal(\"fff[i]blah[/i] f f8whef  98wy 8wyef [image=blah.jpg]asfd [image=\\\"blah.jpg\\\"]affs[i]blah[/i]\", result);\n\t}\n\n\t[Fact]\n\tpublic void CloseUnclosedTag()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"eorifj e oeihf eorhf [b]eoeirf eriojf\");\n\t\tAssert.Equal(\"eorifj e oeihf eorhf [b]eoeirf eriojf[/b]\", result);\n\t}\n\n\t[Fact]\n\tpublic void CloseMultipleUnclosedTags()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"eo[ul]rifj [i]e[/i] oeihf eorhf [b]eoeirf [i]eriojf\");\n\t\tAssert.Equal(\"eo[ul]rifj [i]e[/i] oeihf eorhf [b]eoeirf [i]eriojf[/i][/b][/ul]\", result);\n\t}\n\n\t[Fact]\n\tpublic void CleanUpOverlappingTags()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"eo[ul]rifj [i]e[/ul]asdfg[/i] oeihf eorhf [b]eoeirf [i]eriojf[/i][/b]\");\n\t\tAssert.Equal(\"eo[ul]rifj [i]e[/i][/ul][i]asdfg[/i] oeihf eorhf [b]eoeirf [i]eriojf[/i][/b]\", result);\n\t}\n\n\t[Fact]\n\tpublic void CleanUpOverlappingTags2()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"now is [i]the time to [b]write good[/i] tests [li]and make[/b] sure that [b][i]everything[/li] is awesome[/i][/b] and stuff\");\n\t\tAssert.Equal(\"now is [i]the time to [b]write good[/b][/i][b] tests [li]and make[/li][/b][li] sure that [b][i]everything[/i][/b][/li][b][i] is awesome[/i][/b] and stuff\", result);\n\t}\n\n\t[Fact]\n\tpublic void ClosingTagWithoutOpener()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf whfwofhw h whfweohf[b]oihfowihfwf[/b] ihfwhf [/i]\");\n\t\tAssert.Equal(\"[i]ohf whfwofhw h whfweohf[b]oihfowihfwf[/b] ihfwhf [/i]\", result);\n\t}\n\n\t[Fact]\n\tpublic void ClosingTagWithoutOpener2()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf whfwofhw h whfweohf[b]oihfowihfwf[/b] ihfwhf [/i][/li]\");\n\t\tAssert.Equal(\"[li][i]ohf whfwofhw h whfweohf[b]oihfowihfwf[/b] ihfwhf [/i][/li]\", result);\n\t}\n\n\t[Fact]\n\tpublic void UrlTagOk()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf whfwofhw h whfweohf[url=\\\"http://popw.com/\\\"]oihfo[b]wihfwf[/url] ihfwhf[/b]\");\n\t\tAssert.Equal(\"ohf whfwofhw h whfweohf[url=\\\"http://popw.com/\\\"]oihfo[b]wihfwf[/b][/url][b] ihfwhf[/b]\", result);\n\t}\n\n\t[Fact]\n\tpublic void IgnoreInvalidTag()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf i oih hgoehi[bad]gaeiorw iowh owahfaowhfwohf\");\n\t\tAssert.Equal(\"ohf i oih hgoehi[bad]gaeiorw iowh owahfaowhfwohf\", result);\n\t}\n\n\t[Fact]\n\tpublic void TagUrlWithProtocol()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf i oih hgoehi http://popw.com/ owahfaowhfwohf\");\n\t\tAssert.Equal(\"ohf i oih hgoehi [url=http://popw.com/]http://popw.com/[/url] owahfaowhfwohf\", result);\n\t}\n\n\t[Fact]\n\tpublic void TagLongUrlWithProtocol()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf i oih hgoehi http://popw.com/1234567890123456789012345678901234567890123456789012345678901234567890 owahfaowhfwohf\");\n\t\tAssert.Equal(\"ohf i oih hgoehi [url=http://popw.com/1234567890123456789012345678901234567890123456789012345678901234567890]http://popw.com/12345678901...1234567890[/url] owahfaowhfwohf\", result);\n\t}\n\n\t[Fact]\n\tpublic void TagWwwUrl()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf i oih hgoehi www.popw.com owahfaowhfwohf\");\n\t\tAssert.Equal(\"ohf i oih hgoehi [url=https://www.popw.com]www.popw.com[/url] owahfaowhfwohf\", result);\n\t}\n\n\t[Fact]\n\tpublic void TagLongWwwUrl()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf i oih hgoehi www.popw.com/1234567890123456789012345678901234567890123456789012345678901234567890 owahfaowhfwohf\");\n\t\tAssert.Equal(\"ohf i oih hgoehi [url=https://www.popw.com/1234567890123456789012345678901234567890123456789012345678901234567890]www.popw.com/12345678901234...1234567890[/url] owahfaowhfwohf\", result);\n\t}\n\n\t[Fact]\n\tpublic void TagEmailUrl()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf i oih hgoehi jeff@popw.com owahfaowhfwohf\");\n\t\tAssert.Equal(\"ohf i oih hgoehi [url=mailto:jeff@popw.com]jeff@popw.com[/url] owahfaowhfwohf\", result);\n\t}\n\n\t[Fact]\n\tpublic void EscapeHtml()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"ohf i oih hgoehi <a href=\\\"javascript:alert('blah')\\\">indeed</a> owahfaowhfwohf\");\n\t\tAssert.Equal(\"ohf i oih hgoehi &lt;a href=\\\"javascript:alert('blah')\\\"&gt;indeed&lt;/a&gt; owahfaowhfwohf\", result);\n\t}\n\n\t[Fact]\n\tpublic void DontCreateUrlOpenForOrphanCloser()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"test [b]test[/url] test[/b]\");\n\t\tAssert.Equal(\"test [b]test test[/b]\", result);\n\t}\n\n\t[Fact]\n\tpublic void DoubleHttpArchiveUrl()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"blah http://web.archive.org/web/20001002225219/http://coasterbuzz.com/forums/ blah\");\n\t\tAssert.Equal(\"blah [url=http://web.archive.org/web/20001002225219/http://coasterbuzz.com/forums/]http://web.archive.org/web/...om/forums/[/url] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void DoubleHttpArchiveUrl2()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCode(\"[url=https://web.archive.org/web/20120703090507/http://coasterbuzz.com/Forums/ForumPhoto/PhotoDetail/kings-dominion-2012?p=864820]suck[/url]\");\n\t\tAssert.Equal(\"[url=https://web.archive.org/web/20120703090507/http://coasterbuzz.com/Forums/ForumPhoto/PhotoDetail/kings-dominion-2012?p=864820]suck[/url]\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeHttpOnYouTubeDomain()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah http://youtube.com/watch?v=12345 blah\");\n\t\tAssert.Equal(\"blah [youtube=http://youtube.com/watch?v=12345] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeHttpOnWwwYouTubeDomain()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah http://www.youtube.com/watch?v=12345 blah\");\n\t\tAssert.Equal(\"blah [youtube=http://www.youtube.com/watch?v=12345] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeHttpsOnYouTubeDomain()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah https://youtube.com/watch?v=12345 blah\");\n\t\tAssert.Equal(\"blah [youtube=https://youtube.com/watch?v=12345] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeHttpsOnWwwYouTubeDomain()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah https://www.youtube.com/watch?v=12345 blah\");\n\t\tAssert.Equal(\"blah [youtube=https://www.youtube.com/watch?v=12345] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeHttpOnShortYouTubeDomain()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah http://youtu.be/12345 blah\");\n\t\tAssert.Equal(\"blah [youtube=http://youtu.be/12345] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeHttpsOnShortYouTubeDomain()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah https://youtu.be/12345 blah\");\n\t\tAssert.Equal(\"blah [youtube=https://youtu.be/12345] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeDoesntEmbedWhenUrlIsForShorts()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah https://youtube.com/shorts/0c-5EGX6B2E?si=8OoYCc7x5x_rHiVM blah\");\n\t\tAssert.Equal(\"blah [url=https://youtube.com/shorts/0c-5EGX6B2E?si=8OoYCc7x5x_rHiVM]https://youtube.com/shorts/...7x5x_rHiVM[/url] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeDoesntEmbedWhenUrlIsForChannel()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah https://www.youtube.com/@sillynonsense blah\");\n\t\tAssert.Equal(\"blah [url=https://www.youtube.com/@sillynonsense]https://www.youtube.com/@sillynonsense[/url] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeDoesntEmbedWhenUrlIsForPost()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah https://www.youtube.com/post/Ugkx-hR1fdEJqSWSGTwzpoU4GcT_4ktnY_Qy blah\");\n\t\tAssert.Equal(\"blah [url=https://www.youtube.com/post/Ugkx-hR1fdEJqSWSGTwzpoU4GcT_4ktnY_Qy]https://www.youtube.com/pos...T_4ktnY_Qy[/url] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeLinkParsedToLinkWithImagesOff()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = false;\n\t\tvar result = service.CleanForumCode(\"blah https://youtu.be/12345 blah\");\n\t\tAssert.Equal(\"blah [url=https://youtu.be/12345]https://youtu.be/12345[/url] blah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeLinkInUrlTagNotParsed()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCode(\"blah [url=https://youtu.be/12345]test[/url] blah\");\n\t\tAssert.Equal(\"blah [url=https://youtu.be/12345]test[/url] blah\", result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/TextParsingServiceClientHtmlToForumCodeTests.cs",
    "content": "using SixLabors.ImageSharp;\n\nnamespace PopForums.Test.Services;\n\npublic class TextParsingServiceClientHtmlToForumCodeTests\n{\n\tprivate TextParsingService GetService()\n\t{\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_settings = new Settings();\n\t\t_mockSettingsManager.Current.Returns(_settings);\n\t\treturn new TextParsingService(_mockSettingsManager);\n\t}\n\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate Settings _settings;\n\n\t[Fact]\n\tpublic void RemoveLineBreaks()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test</p>\\r\\n<p>test</p>\");\n\t\tAssert.Equal(\"test\\r\\n\\r\\ntest\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveEmptyLinesWithOnlySpace()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>&nbsp;</p>\\r\\n<p>&nbsp;</p>\");\n\t\tAssert.Equal(result, string.Empty);\n\t}\n\n\t[Fact]\n\tpublic void DitchStartAndEndPara()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test</p>\");\n\t\tAssert.Equal(\"test\", result);\n\t}\n\n\t[Fact]\n\tpublic void PutQuoteOnItsOwnLines()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test</p><blockquote>quote</blockquote><p>test</p>\");\n\t\tAssert.Equal(\"test\\r\\n\\r\\n[quote]\\r\\nquote\\r\\n[/quote]\\r\\n\\r\\ntest\", result);\n\t}\n\n\t[Fact]\n\tpublic void StartAndEndParaWithBreaks()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test</p><p>test</p>\");\n\t\tAssert.Equal(\"test\\r\\n\\r\\ntest\", result);\n\t}\n\n\t[Fact]\n\tpublic void SingleLineBreaks()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test<br/>test<br />test<br>test</p>\");\n\t\tAssert.Equal(\"test\\r\\ntest\\r\\ntest\\r\\ntest\", result);\n\t}\n\n\t[Fact]\n\tpublic void LineBreakInAndOutOfQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<blockquote>quote</blockquote>\");\n\t\tAssert.Equal(\"[quote]\\r\\nquote\\r\\n[/quote]\", result);\n\t}\n\n\t[Fact]\n\tpublic void ItalicVariations()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <em>test</Em> <I>test</i> test</p>\");\n\t\tAssert.Equal(\"test [i]test[/i] [i]test[/i] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void BoldVariations()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <sTrOnG>test</Strong> <b>test</B> test</p>\");\n\t\tAssert.Equal(\"test [b]test[/b] [b]test[/b] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void Code()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <cOde>test</code> test</p>\");\n\t\tAssert.Equal(\"test [code]test[/code] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void Pre()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <Pre>test</pRe> test</p>\");\n\t\tAssert.Equal(\"test [pre]test[/pre] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void Li()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <LI>test</lI> test</p>\");\n\t\tAssert.Equal(\"test [li]test[/li] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void Ol()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <oL>test</Ol> test</p>\");\n\t\tAssert.Equal(\"test [ol]test[/ol] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void Ul()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <uL>test</Ul> test</p>\");\n\t\tAssert.Equal(\"test [ul]test[/ul] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void AnchorToUrl()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <a href=\\\"http://popw.com/\\\">test</a> test</p>\");\n\t\tAssert.Equal(\"test [url=http://popw.com/]test[/url] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void AnchorToUrlWithTarget()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <a href=\\\"http://popw.com/\\\" target=\\\"_blank\\\">test</a> test</p>\");\n\t\tAssert.Equal(\"test [url=http://popw.com/]test[/url] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void AnchorWithDoubleProtocol()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p><a href=\\\"https://web.archive.org/web/20120703090507/http://coasterbuzz.com/Forums/ForumPhoto/PhotoDetail/kings-dominion-2012?p=864820\\\">suck</a></p>\");\n\t\tAssert.Equal(\"[url=https://web.archive.org/web/20120703090507/http://coasterbuzz.com/Forums/ForumPhoto/PhotoDetail/kings-dominion-2012?p=864820]suck[/url]\", result);\n\t}\n\n\t[Fact]\n\tpublic void AnchorToUrlWithTargetNoQuotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <a href=\\\"http://popw.com/\\\" target=_blank>test</a> test</p>\");\n\t\tAssert.Equal(\"test [url=http://popw.com/]test[/url] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void ImageNoClose()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <img src=\\\"blah.jpg\\\" alt=\\\"\\\"> test</p>\");\n\t\tAssert.Equal(\"test [image=blah.jpg] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void ImageNoCloseNoSpace()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <img src=\\\"blah.jpg\\\"/> test</p>\");\n\t\tAssert.Equal(\"test [image=blah.jpg] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void ImageNoCloseSpace()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <img src=\\\"blah.jpg\\\" /> test</p>\");\n\t\tAssert.Equal(\"test [image=blah.jpg] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void ImageOtherAttribute()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <img randomatter=\\\"swegf\\\" src=\\\"blah.jpg\\\" /> test</p>\");\n\t\tAssert.Equal(\"test [image=blah.jpg] test\", result);\n\t\tresult = service.ClientHtmlToForumCode(\"<p>test <img randomatter=\\\"swegf\\\" omg=\\\"wef\\\" src=\\\"blah.jpg\\\" /> test</p>\");\n\t\tAssert.Equal(\"test [image=blah.jpg] test\", result);\n\t}\n\t\n\t[Fact]\n\tpublic void ImageOtherAttributeAfterSrc()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(@\"<p>image:</p>\n<p><img src=\"\"https://popforums.com/img/hugcloud.png\"\" alt=\"\"\"\"></p>\");\n\t\tAssert.Equal(\"image:\\r\\n\\r\\n[image=https://popforums.com/img/hugcloud.png]\", result);\n\t}\n\n\t[Fact]\n\tpublic void ImageOtherAttributeBeforeAndAfterSrc()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <img randomatter=\\\"swegf\\\" omg=\\\"wef\\\" src=\\\"blah.jpg\\\" randomatter=\\\"swegf\\\" omg=\\\"wef\\\" /> test</p>\");\n\t\tAssert.Equal(\"test [image=blah.jpg] test\", result);\n\t}\n\n\t[Fact]\n\tpublic void NukeInvalidHtml()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test <script>alert('blah');</script> test</p>\");\n\t\tAssert.Equal(\"test alert('blah'); test\", result);\n\t}\n\n\t[Fact]\n\tpublic void NukeInvalidHtml2()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>[quote]<em>WolfBobs said:</em><br><span style=\\\"font-size: 1rem;\\\">People need to learn to not blindly follow people based on red and blue. That's why our country has been stagnant with no real change for the good of the people for decades. Good for corporations? Sure. But the people? Not so much.</span><br></p><p>[/quote]It's reassuring to see that some people feel that way.</p>\");\n\t\tAssert.Equal(\"[quote]\\r\\n[i]WolfBobs said:[/i]\\r\\nPeople need to learn to not blindly follow people based on red and blue. That's why our country has been stagnant with no real change for the good of the people for decades. Good for corporations? Sure. But the people? Not so much.\\r\\n\\r\\n\\r\\n[/quote]It's reassuring to see that some people feel that way.\", result);\n\t}\n\n\t[Fact]\n\tpublic void ConvertHtmlEscapes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test &lt; &gt; &amp; &nbsp; test</p>\");\n\t\tAssert.Equal(\"test < > &   test\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveLineBreaksInList()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>blah</p><ul>\\n<li>first</li>\\n<li>second</li>\\n<li>third</li>\\n</ul><p>blah</p>\");\n\t\tAssert.Equal(\"blah\\r\\n\\r\\n[ul][li]first[/li][li]second[/li][li]third[/li][/ul]\\r\\n\\r\\nblah\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeUnparse()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test</p><p><iframe width=\\\"123\\\" height=\\\"545\\\" src=\\\"http://www.youtube.com/embed/789\\\" frameborder=\\\"0\\\" allowfullscreen></iframe></p><p>test</p>\");\n\t\tAssert.Equal(\"test\\r\\n\\r\\n[youtube=https://www.youtube.com/watch?v=789]\\r\\n\\r\\ntest\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeUnparseHttps()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test</p><p><iframe width=\\\"123\\\" height=\\\"545\\\" src=\\\"https://www.youtube.com/embed/789\\\" frameborder=\\\"0\\\" allowfullscreen></iframe></p><p>test</p>\");\n\t\tAssert.Equal(\"test\\r\\n\\r\\n[youtube=https://www.youtube.com/watch?v=789]\\r\\n\\r\\ntest\", result);\n\t}\n\n\t[Fact]\n\tpublic void ParseImageWithExtraAttributesLikeAlt()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p>test</p>\\r\\n<p><img src=\\\"https://scontent.ftpa1-2.fna.fbcdn.net/v/t31.0-8/12119905_10153331542212955_4087525267669435874_o.jpg?_nc_cat=104&amp;_nc_ht=scontent.ftpa1-2.fna&amp;oh=bde1d73b39027f410a9506c19dfb4428&amp;oe=5D95ACD5\\\" alt=\\\"\\\" /></p><p>test</p>\");\n\t\tAssert.Equal(\"test\\r\\n\\r\\n[image=https://scontent.ftpa1-2.fna.fbcdn.net/v/t31.0-8/12119905_10153331542212955_4087525267669435874_o.jpg?_nc_cat=104&_nc_ht=scontent.ftpa1-2.fna&oh=bde1d73b39027f410a9506c19dfb4428&oe=5D95ACD5]\\r\\n\\r\\ntest\", result);\n\t}\n\n\t[Fact]\n\tpublic void ParseSequentialImages()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p><img src=\\\"test1.jpg\\\" /><img src=\\\"test2.jpg\\\" /></p>\");\n\t\tAssert.Equal(\"[image=test1.jpg][image=test2.jpg]\", result);\n\t}\n\n\t[Fact]\n\tpublic void ParseNonSequentialImages()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ClientHtmlToForumCode(\"<p><img src=\\\"test1.jpg\\\" /></p><p><img src=\\\"test2.jpg\\\" /></p>\");\n\t\tAssert.Equal(\"[image=test1.jpg]\\r\\n\\r\\n[image=test2.jpg]\", result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/TextParsingServiceForumCodeToHtmlTests.cs",
    "content": "namespace PopForums.Test.Services;\n\npublic class TextParsingServiceForumCodeToHtmlTests\n{\n\tprivate TextParsingService GetService()\n\t{\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_settings = new Settings();\n\t\t_mockSettingsManager.Current.Returns(_settings);\n\t\treturn new TextParsingService(_mockSettingsManager);\n\t}\n\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate Settings _settings;\n\t\t\n\t[Fact]\n\tpublic void UrlWithQuotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [url=\\\"http://popw.com/\\\"]my link[/url].\");\n\t\tAssert.Equal(\"<p>this is <a href=\\\"http://popw.com/\\\" target=\\\"_blank\\\">my link</a>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void UrlWithoutQuotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [url=http://popw.com/]my link[/url].\");\n\t\tAssert.Equal(\"<p>this is <a href=\\\"http://popw.com/\\\" target=\\\"_blank\\\">my link</a>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void MailLinkWithQuotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [url=\\\"mailto:jeff@popw.com\\\"]my link[/url].\");\n\t\tAssert.Equal(\"<p>this is <a href=\\\"mailto:jeff@popw.com\\\">my link</a>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void MailLinkWithoutQuotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [url=mailto:jeff@popw.com]my link[/url].\");\n\t\tAssert.Equal(\"<p>this is <a href=\\\"mailto:jeff@popw.com\\\">my link</a>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void DitchNaughtyJavascriptLinkWithQuotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [url=\\\"javascript:alert('blah')\\\"]my link[/url].\");\n\t\tAssert.Equal(\"<p>this is <a href=\\\"alert('blah')\\\" target=\\\"_blank\\\">my link</a>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void DitchNaughtyJavascriptLinkWithoutQuotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [url=javascript:alert('blah')]my link[/url].\");\n\t\tAssert.Equal(\"<p>this is <a href=\\\"alert('blah')\\\" target=\\\"_blank\\\">my link</a>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void DitchNaughtyJavascriptLinkUpperCase()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [url=JAVASCRIPT:alert('blah')]my link[/url].\");\n\t\tAssert.Equal(\"<p>this is <a href=\\\"alert('blah')\\\" target=\\\"_blank\\\">my link</a>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void DitchNaughtyJavascriptLinkMixedCase()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [url=Javascript:alert('blah')]my link[/url].\");\n\t\tAssert.Equal(\"<p>this is <a href=\\\"alert('blah')\\\" target=\\\"_blank\\\">my link</a>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplaceImageTagsWithQuotes()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCodeToHtml(\"check out the image [image=\\\"my.jpg\\\"] here\");\n\t\tAssert.Equal(\"<p>check out the image <img src=\\\"my.jpg\\\" /> here</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplaceImageTagsWithoutQuotes()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCodeToHtml(\"check out the image [image=my.jpg] here\");\n\t\tAssert.Equal(\"<p>check out the image <img src=\\\"my.jpg\\\" /> here</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveImageTagsWithQuotes()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = false;\n\t\tvar result = service.CleanForumCodeToHtml(\"check out the image [image=\\\"my.jpg\\\"] here\");\n\t\tAssert.Equal(\"<p>check out the image  here</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveImageTagsWithoutQuotes()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = false;\n\t\tvar result = service.CleanForumCodeToHtml(\"check out the image [image=my.jpg] here\");\n\t\tAssert.Equal(\"<p>check out the image  here</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ParseClassicImgTags()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCodeToHtml(\"check out the image [img]http://coasterbuzz.com/CoasterPhoto/CoasterPhotoImage/4800[/img] here\");\n\t\tAssert.Equal(\"<p>check out the image <img src=\\\"http://coasterbuzz.com/CoasterPhoto/CoasterPhotoImage/4800\\\" /> here</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ParseAllThreeImageVariants()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCodeToHtml(\"[image=http://coasterbuzz.com/CoasterPhoto/CoasterPhotoImage/4800]\\r\\n\\r\\n[image=\\\"http://coasterbuzz.com/CoasterPhoto/CoasterPhotoImage/4800\\\"]\\r\\n\\r\\n[img]http://coasterbuzz.com/CoasterPhoto/CoasterPhotoImage/4800[/img]\");\n\t\tAssert.Equal(\"<p><img src=\\\"http://coasterbuzz.com/CoasterPhoto/CoasterPhotoImage/4800\\\" /></p><p><img src=\\\"http://coasterbuzz.com/CoasterPhoto/CoasterPhotoImage/4800\\\" /></p><p><img src=\\\"http://coasterbuzz.com/CoasterPhoto/CoasterPhotoImage/4800\\\" /></p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplaceItalic()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [i]italic[/i].\");\n\t\tAssert.Equal(\"<p>this is <em>italic</em>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplaceBold()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [b]bold[/b].\");\n\t\tAssert.Equal(\"<p>this is <strong>bold</strong>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplaceCode()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [code]code[/code].\");\n\t\tAssert.Equal(\"<p>this is <code>code</code>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplacePre()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [pre]pre[/pre].\");\n\t\tAssert.Equal(\"<p>this is <pre>pre</pre>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplaceLi()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [li]li[/li].\");\n\t\tAssert.Equal(\"<p>this is <li>li</li>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplaceOl()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [ol]ol[/ol].\");\n\t\tAssert.Equal(\"<p>this is <ol>ol</ol>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void ReplaceUl()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [ul]ul[/ul].\");\n\t\tAssert.Equal(\"<p>this is <ul>ul</ul>.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void SurroundWithPara()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is some text.\");\n\t\tAssert.Equal(\"<p>this is some text.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void NoParaIfStartsOrEndWithQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[quote]this is some text.[/quote]\");\n\t\tAssert.False(result.StartsWith(\"<p>\") || result.EndsWith(\"</p>\"));\n\t}\n\n\t[Fact]\n\tpublic void ReplaceQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is [quote]some[/quote] text.\");\n\t\tAssert.Equal(\"<p>this is </p><blockquote><p>some</p></blockquote><p> text.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void DoubleLineBreakToParaEndStart()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is\\r\\n\\r\\nsome text.\");\n\t\tAssert.Equal(\"<p>this is</p><p>some text.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void NoDoubleLineBreakToParaEndStartIfAtQuoteEnd()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[quote]this is[/quote]\\r\\n\\r\\nsome text.\");\n\t\tAssert.Equal(\"<blockquote><p>this is</p></blockquote><p>some text.</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void NoDoubleLineBreakToParaEndStartIfAtQuoteStart()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"this is\\r\\n\\r\\n[quote]some text.[/quote]\");\n\t\tAssert.Equal(\"<p>this is</p><blockquote><p>some text.</p></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void EliminateLineBreaksBetweenEndParaAndQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"test text\\r\\n[quote]test quote[/quote]test text\\r\\n\\r\\n[quote]test quote[/quote]\");\n\t\tAssert.Equal(\"<p>test text</p><blockquote><p>test quote</p></blockquote><p>test text</p><blockquote><p>test quote</p></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void EliminateLineBreaksBetweenEndQuoteAndQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"test text\\r\\n[quote]test quote[/quote]\\r\\n\\r\\n[quote]test quote[/quote]\");\n\t\tAssert.Equal(\"<p>test text</p><blockquote><p>test quote</p></blockquote><blockquote><p>test quote</p></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void EliminateLineBreaksBetweenStartAndQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"\\r\\n\\r\\n[quote]test quote[/quote]\");\n\t\tAssert.Equal(\"<blockquote><p>test quote</p></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void CloseParaBeforeQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"test text\\r\\n[quote]test quote[/quote]test text\\r\\n\\r\\n[quote]test quote[/quote]test text[quote]test quote[/quote]\");\n\t\tAssert.Equal(\"<p>test text</p><blockquote><p>test quote</p></blockquote><p>test text</p><blockquote><p>test quote</p></blockquote><p>test text</p><blockquote><p>test quote</p></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void StartInsideOfQuoteWithParaUnlessFirstThingIsSubQuoteOrPara()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[quote]test quote[/quote][quote][quote]test quote[/quote][/quote]\");\n\t\tAssert.Equal(\"<blockquote><p>test quote</p></blockquote><blockquote><blockquote><p>test quote</p></blockquote></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void EndInsideOfQuoteWithParaUnlessFirstThingIsSubQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[quote]test quote[/quote][quote][quote]test quote[/quote][/quote]\");\n\t\tAssert.Equal(\"<blockquote><p>test quote</p></blockquote><blockquote><blockquote><p>test quote</p></blockquote></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void StartParaAfterQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[quote]test quote[/quote]test text[quote]test quote[/quote]\\r\\ntest text[quote]test quote[/quote]\\r\\n\\r\\ntest text\");\n\t\tAssert.Equal(\"<blockquote><p>test quote</p></blockquote><p>test text</p><blockquote><p>test quote</p></blockquote><p>test text</p><blockquote><p>test quote</p></blockquote><p>test text</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveTrailingLineBreaksBetweenEndsOfQuotes()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[quote][quote]test quote[/quote]\\r\\n\\r\\n[/quote][quote][quote]test quote[/quote]\\r\\n[/quote]\");\n\t\tAssert.Equal(\"<blockquote><blockquote><p>test quote</p></blockquote></blockquote><blockquote><blockquote><p>test quote</p></blockquote></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void EndParaInQuote()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[quote]test quote[/quote][quote]test quote\\r\\n[/quote][quote]test quote\\r\\n\\r\\n[/quote]\");\n\t\tAssert.Equal(\"<blockquote><p>test quote</p></blockquote><blockquote><p>test quote</p></blockquote><blockquote><p>test quote</p></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void StartParaAfterQuote2()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[quote]test quote[/quote]test text[quote]test quote[/quote]\\r\\ntest text[quote]test quote[/quote]\\r\\n\\r\\ntest text\");\n\t\tAssert.Equal(\"<blockquote><p>test quote</p></blockquote><p>test text</p><blockquote><p>test quote</p></blockquote><p>test text</p><blockquote><p>test quote</p></blockquote><p>test text</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void SingleLineBreak()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"test text\\r\\ntest text\");\n\t\tAssert.Equal(\"<p>test text<br />test text</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void DoubleHttpArchiveUrl()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"blah [url=http://web.archive.org/web/20001002225219/http://coasterbuzz.com/forums/]http://web.archive.org/web/20001002225219/http://coasterbuzz.com/forums/[/url] blah\");\n\t\tAssert.Equal(\"<p>blah <a href=\\\"http://web.archive.org/web/20001002225219/http://coasterbuzz.com/forums/\\\" target=\\\"_blank\\\">http://web.archive.org/web/20001002225219/http://coasterbuzz.com/forums/</a> blah</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void HttpsAndHttpArchiveUrlWithTextLink()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(\"[url=https://web.archive.org/web/20120703090507/http://coasterbuzz.com/Forums/ForumPhoto/PhotoDetail/kings-dominion-2012?p=864820]sucks[/url]\");\n\t\tAssert.Equal(\"<p><a href=\\\"https://web.archive.org/web/20120703090507/http://coasterbuzz.com/Forums/ForumPhoto/PhotoDetail/kings-dominion-2012?p=864820\\\" target=\\\"_blank\\\">sucks</a></p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeTagMainDomainConvertedToIframe()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.YouTubeHeight = 123;\n\t\t_settings.YouTubeWidth = 456;\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCodeToHtml(\"test [youtube=http://youtube.com/watch?v=789] text\");\n\t\tAssert.Equal(\"<p>test <iframe width=\\\"456\\\" height=\\\"123\\\" src=\\\"https://www.youtube.com/embed/789\\\" frameborder=\\\"0\\\" allowfullscreen></iframe> text</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeTagMainDomainHttpsConvertedToIframe()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.YouTubeHeight = 123;\n\t\t_settings.YouTubeWidth = 456;\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCodeToHtml(\"test [youtube=https://youtube.com/watch?v=789] text\");\n\t\tAssert.Equal(\"<p>test <iframe width=\\\"456\\\" height=\\\"123\\\" src=\\\"https://www.youtube.com/embed/789\\\" frameborder=\\\"0\\\" allowfullscreen></iframe> text</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeTagShortDomainConvertedToIframe()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.YouTubeHeight = 123;\n\t\t_settings.YouTubeWidth = 456;\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCodeToHtml(\"test [youtube=http://youtu.be/789] text\");\n\t\tAssert.Equal(\"<p>test <iframe width=\\\"456\\\" height=\\\"123\\\" src=\\\"https://www.youtube.com/embed/789\\\" frameborder=\\\"0\\\" allowfullscreen></iframe> text</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeTagShortDomainHttpsConvertedToIframe()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.YouTubeHeight = 123;\n\t\t_settings.YouTubeWidth = 456;\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.CleanForumCodeToHtml(\"test [youtube=https://youtu.be/789] text\");\n\t\tAssert.Equal(\"<p>test <iframe width=\\\"456\\\" height=\\\"123\\\" src=\\\"https://www.youtube.com/embed/789\\\" frameborder=\\\"0\\\" allowfullscreen></iframe> text</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeTagConvertedToIframe()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.YouTubeHeight = 123;\n\t\t_settings.YouTubeWidth = 456;\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.ForumCodeToHtml(\"test test [youtube=http://www.youtube.com/watch?v=NL125lBWYc4] test\");\n\t\tAssert.Equal(\"<p>test test <iframe width=\\\"456\\\" height=\\\"123\\\" src=\\\"https://www.youtube.com/embed/NL125lBWYc4\\\" frameborder=\\\"0\\\" allowfullscreen></iframe> test</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubeTagHttpsConvertedToIframe()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.YouTubeHeight = 123;\n\t\t_settings.YouTubeWidth = 456;\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.ForumCodeToHtml(\"test test [youtube=https://www.youtube.com/watch?v=NL125lBWYc4] test\");\n\t\tAssert.Equal(\"<p>test test <iframe width=\\\"456\\\" height=\\\"123\\\" src=\\\"https://www.youtube.com/embed/NL125lBWYc4\\\" frameborder=\\\"0\\\" allowfullscreen></iframe> test</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void YouTubePostUrlParsedAsRegularLink()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.YouTubeHeight = 123;\n\t\t_settings.YouTubeWidth = 456;\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.ForumCodeToHtml(\"test https://www.youtube.com/post/Ugkx-hR1fdEJqSWSGTwzpoU4GcT_4ktnY_Qy text\");\n\t\tAssert.Equal(\"<p>test <a href=\\\"https://www.youtube.com/post/Ugkx-hR1fdEJqSWSGTwzpoU4GcT_4ktnY_Qy\\\" target=\\\"_blank\\\">https://www.youtube.com/pos...T_4ktnY_Qy</a> text</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void UrlWithBangParsesCorrectly()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ForumCodeToHtml(\"(and [url=\\\"https://groups.google.com/forum/#!original/rec.roller-coaster/iwTIvU2IXKI/hKB_D9uRbaEJ\\\"]'millennium' is spelled with two n's[/url] regardless of whether the new one starts in 2000 or 2001.)\");\n\t\tAssert.Equal(\"<p>(and <a href=\\\"https://groups.google.com/forum/#!original/rec.roller-coaster/iwTIvU2IXKI/hKB_D9uRbaEJ\\\" target=\\\"_blank\\\">'millennium' is spelled with two n's</a> regardless of whether the new one starts in 2000 or 2001.)</p>\", result);\n\t}\n\t\n\t[Fact]\n\tpublic void UrlWithParenthesesParsesCorrectly()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ForumCodeToHtml(\"(and [url=\\\"https://blahblah.com/test(test)test\\\"]'millennium' is spelled with two n's[/url] regardless of whether the new one starts in 2000 or 2001.)\");\n\t\tAssert.Equal(\"<p>(and <a href=\\\"https://blahblah.com/test(test)test\\\" target=\\\"_blank\\\">'millennium' is spelled with two n's</a> regardless of whether the new one starts in 2000 or 2001.)</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void DontParagraphAnEmptyString()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.CleanForumCodeToHtml(string.Empty);\n\t\tAssert.Equal(result, string.Empty);\n\t}\n\n\t[Fact]\n\tpublic void UrlWithParenthesesParsed()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.ForumCodeToHtml(\"blah https://blahblah.com/test(test)test blah\");\n\t\tAssert.Equal(\"<p>blah <a href=\\\"https://blahblah.com/test(test)test\\\" target=\\\"_blank\\\">https://blahblah.com/test(test)test</a> blah</p>\", result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/TextParsingServiceOtherTests.cs",
    "content": "namespace PopForums.Test.Services;\n\npublic class TextParsingServiceOtherTests\n{\n\tprivate TextParsingService GetService()\n\t{\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_settings = new Settings();\n\t\t_mockSettingsManager.Current.Returns(_settings);\n\t\treturn new TextParsingService(_mockSettingsManager);\n\t}\n\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate Settings _settings;\n\n\t[Fact]\n\tpublic void ConvertHtmlQuoteToForumCodeQuote()\n\t{\n\t\t// yes, this is a test to avoid old branches that treated quotes as BB code relics\n\t\tvar service = GetService();\n\t\tvar result = service.HtmlToClientHtml(\"<p>some text</p><blockquote><p>quote text</p></blockquote>\");\n\t\tAssert.Equal(\"<p>some text</p><blockquote><p>quote text</p></blockquote>\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveAnchorTargets()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.HtmlToClientHtml(\"<p>some text <a href=\\\"http://popw.com/\\\" target=\\\"what_ever\\\">link</a></p>\");\n\t\tAssert.Equal(\"<p>some text <a href=\\\"http://popw.com/\\\">link</a></p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveYouTubeIframe()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.HtmlToClientHtml(\"<p>test test <iframe width=\\\"640\\\" height=\\\"360\\\" src=\\\"http://www.youtube.com/embed/NL125lBWYc4\\\" frameborder=\\\"0\\\" allowfullscreen></iframe> test</p>\");\n\t\tAssert.Equal(\"<p>test test https://www.youtube.com/watch?v=NL125lBWYc4 test</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void RemoveYouTubeIframeHttps()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.HtmlToClientHtml(\"<p>test test <iframe width=\\\"640\\\" height=\\\"360\\\" src=\\\"https://www.youtube.com/embed/NL125lBWYc4\\\" frameborder=\\\"0\\\" allowfullscreen></iframe> test</p>\");\n\t\tAssert.Equal(\"<p>test test https://www.youtube.com/watch?v=NL125lBWYc4 test</p>\", result);\n\t}\n\n\t[Fact]\n\tpublic void CensorTheNaughty()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.CensorWords = \"shit bitch  fuck\";\n\t\t_settings.CensorCharacter = \"+\";\n\t\tvar result = service.Censor(\"this is some shitty fucked up code, bitch\");\n\t\tAssert.Equal(\"this is some ++++ty ++++ed up code, +++++\", result);\n\t}\n\n\t[Fact]\n\tpublic void CensorTheNaughtyCaseInsensitive()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.CensorWords = \"shit bitch  fuck\";\n\t\t_settings.CensorCharacter = \"+\";\n\t\tvar result = service.Censor(\"this is some sHitty FucKed up code, bitCH\");\n\t\tAssert.Equal(\"this is some ++++ty ++++ed up code, +++++\", result);\n\t}\n\n\t[Fact]\n\tpublic void CensorEmptyReturnsEmpty()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.Censor(String.Empty);\n\t\tAssert.Equal(String.Empty, result);\n\t}\n\n\t[Fact]\n\tpublic void CensorNullReturnsEmpty()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.Censor(null);\n\t\tAssert.Equal(String.Empty, result);\n\t}\n\n\t[Fact]\n\tpublic void ForumCodeToHtmlReturnsEmptyInsteadOfParaTags()\n\t{\n\t\tvar service = GetService();\n\t\tvar result = service.ForumCodeToHtml(\"\");\n\t\tAssert.Equal(String.Empty, result);\n\t}\n\n\t[Fact]\n\tpublic void ParsedUrlWithParenthesesUnparsed()\n\t{\n\t\tvar service = GetService();\n\t\t_settings.AllowImages = true;\n\t\tvar result = service.HtmlToClientHtml(\"<p>blah <a href=\\\"https://blahblah.com/test(test)test\\\" target=\\\"_blank\\\">https://blahblah.com/test(test)test</a> blah</p>\");\n\t\tAssert.Equal(\"<p>blah <a href=\\\"https://blahblah.com/test(test)test\\\">https://blahblah.com/test(test)test</a> blah</p>\", result);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/TopicServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class TopicServiceTests\n{\n\tprivate ISettingsManager _settingsManager;\n\tprivate ITopicRepository _topicRepo;\n\tprivate IPostRepository _postRepo;\n\tprivate IModerationLogService _modService;\n\tprivate IForumService _forumService;\n\tprivate IEventPublisher _eventPublisher;\n\tprivate ISearchRepository _searchRepo;\n\tprivate IUserRepository _userRepo;\n\tprivate ISearchIndexQueueRepository _searchIndexQueueRepo;\n\tprivate ITenantService _tenantService;\n\tprivate INotificationAdapter _notificationAdapter;\n\n\tprivate TopicService GetTopicService()\n\t{\n\t\t_settingsManager = Substitute.For<ISettingsManager>();\n\t\t_topicRepo = Substitute.For<ITopicRepository>();\n\t\t_postRepo = Substitute.For<IPostRepository>();\n\t\t_modService = Substitute.For<IModerationLogService>();\n\t\t_forumService = Substitute.For<IForumService>();\n\t\t_eventPublisher = Substitute.For<IEventPublisher>();\n\t\t_searchRepo = Substitute.For<ISearchRepository>();\n\t\t_userRepo = Substitute.For<IUserRepository>();\n\t\t_searchIndexQueueRepo = Substitute.For<ISearchIndexQueueRepository>();\n\t\t_tenantService = Substitute.For<ITenantService>();\n\t\t_notificationAdapter = Substitute.For<INotificationAdapter>();\n\t\treturn new TopicService(_topicRepo, _postRepo, _settingsManager, _modService, _forumService, _eventPublisher, _searchRepo, _userRepo, _searchIndexQueueRepo, _tenantService, _notificationAdapter);\n\t}\n\n\tprivate static User GetUser()\n\t{\n\t\treturn new User { UserID = 123, Name = \"Name\", Email = \"Email\", IsApproved = true, AuthorizationKey = Guid.NewGuid(), Roles = new List<string>()};\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsFromRepo()\n\t{\n\t\tvar forum = new Forum { ForumID = 1, TopicCount = 3 };\n\t\tvar topicService = GetTopicService();\n\t\tvar repoTopics = new List<Topic>();\n\t\tvar settings = new Settings {TopicsPerPage = 20};\n\t\t_topicRepo.Get(1, true, 1, settings.TopicsPerPage).Returns(Task.FromResult(repoTopics));\n\t\t_settingsManager.Current.Returns(settings);\n\t\tvar (topics, _) = await topicService.GetTopics(forum, true, 1);\n\t\tAssert.Same(repoTopics, topics);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsStartRowCalcd()\n\t{\n\t\tvar forum = new Forum { ForumID = 1, TopicCount = 300 };\n\t\tvar topicService = GetTopicService();\n\t\tvar settings = new Settings { TopicsPerPage = 20 };\n\t\t_settingsManager.Current.Returns(settings);\n\t\tawait topicService.GetTopics(forum, false, 3);\n\t\tawait _topicRepo.Received().Get(Arg.Any<int>(), Arg.Any<bool>(), 41, Arg.Any<int>());\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsIncludeDeletedCallsRepoCount()\n\t{\n\t\tvar forum = new Forum { ForumID = 1 };\n\t\tvar topicService = GetTopicService();\n\t\t_topicRepo.Get(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(new List<Topic>()));\n\t\t_topicRepo.GetTopicCount(1, true).Returns(Task.FromResult(350));\n\t\t_settingsManager.Current.Returns(new Settings());\n\t\tawait topicService.GetTopics(forum, true, 3);\n\t\tawait _topicRepo.Received().GetTopicCount(forum.ForumID, true);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsNotIncludeDeletedNotCallRepoCount()\n\t{\n\t\tvar forum = new Forum { ForumID = 1 };\n\t\tvar topicService = GetTopicService();\n\t\t_topicRepo.Get(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(new List<Topic>()));\n\t\t_settingsManager.Current.Returns(new Settings());\n\t\tawait topicService.GetTopics(forum, false, 3);\n\t\tawait _topicRepo.DidNotReceive().GetTopicCount(forum.ForumID, false);\n\t}\n\n\t[Fact]\n\tpublic async Task GetTopicsPagerContextIncludesPageIndexAndCalcdTotalPages()\n\t{\n\t\tvar forum = new Forum {ForumID = 1, TopicCount = 301};\n\t\tvar forum2 = new Forum {ForumID = 2, TopicCount = 300};\n\t\tvar forum3 = new Forum {ForumID = 3, TopicCount = 299};\n\t\tvar topicService = GetTopicService();\n\t\tvar settings = new Settings { TopicsPerPage = 20 };\n\t\t_topicRepo.Get(Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<int>(), settings.TopicsPerPage).Returns(Task.FromResult(new List<Topic>()));\n\t\t_settingsManager.Current.Returns(settings);\n\t\tvar (_, pagerContext) = await topicService.GetTopics(forum, false, 3);\n\t\tAssert.Equal(3, pagerContext.PageIndex);\n\t\tAssert.Equal(16, pagerContext.PageCount);\n\t\t(_, pagerContext) = await topicService.GetTopics(forum2, false, 4);\n\t\tAssert.Equal(4, pagerContext.PageIndex);\n\t\tAssert.Equal(15, pagerContext.PageCount);\n\t\t(_, pagerContext) = await topicService.GetTopics(forum3, false, 5);\n\t\tAssert.Equal(5, pagerContext.PageIndex);\n\t\tAssert.Equal(15, pagerContext.PageCount);\n\t\tAssert.Equal(settings.TopicsPerPage, pagerContext.PageSize);\n\t}\n\n\t[Fact]\n\tpublic async Task CloseTopicThrowsWithNonMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tvar topicService = GetTopicService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await topicService.CloseTopic(topic, user));\n\t}\n\n\t[Fact]\n\tpublic async Task CloseTopicClosesWithMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tawait topicService.CloseTopic(topic, user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicClose, topic, null);\n\t\tawait _topicRepo.Received(1).CloseTopic(topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task OpenTopicThrowsWithNonMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tvar topicService = GetTopicService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await topicService.OpenTopic(topic, user));\n\t}\n\n\t[Fact]\n\tpublic async Task OpenTopicOpensWithMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tawait topicService.OpenTopic(topic, user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicOpen, topic, null);\n\t\tawait _topicRepo.Received(1).OpenTopic(topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task PinTopicThrowsWithNonMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tvar topicService = GetTopicService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await topicService.PinTopic(topic, user));\n\t}\n\n\t[Fact]\n\tpublic async Task PinTopicPinsWithMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tawait topicService.PinTopic(topic, user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicPin, topic, null);\n\t\tawait _topicRepo.Received(1).PinTopic(topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task UnpinTopicThrowsWithNonMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tvar topicService = GetTopicService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await topicService.UnpinTopic(topic, user));\n\t}\n\n\t[Fact]\n\tpublic async Task UnpinTopicUnpinsWithMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tawait topicService.UnpinTopic(topic, user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicUnpin, topic, null);\n\t\tawait _topicRepo.Received(1).UnpinTopic(topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteTopicThrowsWithNonMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tvar topicService = GetTopicService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await topicService.DeleteTopic(topic, user));\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteTopicDeletesWithMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tawait topicService.DeleteTopic(topic, user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicDelete, topic, null);\n\t\tawait _topicRepo.Received(1).DeleteTopic(topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteTopicUpdatesCounts()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 123 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t_forumService.Get(topic.ForumID).Returns(Task.FromResult(forum));\n\t\tawait topicService.DeleteTopic(topic, user);\n\t\t_forumService.Received(1).UpdateCounts(forum);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteTopicQueuesIndexRemoval()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 123 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t_forumService.Get(topic.ForumID).Returns(Task.FromResult(forum));\n\t\tSearchIndexPayload payload = null;\n\t\tawait _searchIndexQueueRepo.Enqueue(Arg.Do<SearchIndexPayload>(x => payload = x));\n\t\t_tenantService.GetTenant().Returns(\"t\");\n\n\t\tawait topicService.DeleteTopic(topic, user);\n\n\t\tAssert.Equal(topic.TopicID, payload.TopicID);\n\t\tAssert.Equal(\"t\", payload.TenantID);\n\t\tAssert.True(payload.IsForRemoval);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteTopicUpdatesLast()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 123 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t_forumService.Get(topic.ForumID).Returns(Task.FromResult(forum));\n\t\tawait topicService.DeleteTopic(topic, user);\n\t\tawait _forumService.Received(1).UpdateLast(forum);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteTopicUpdatesReplyCount()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\t_postRepo.GetReplyCount(topic.TopicID, false).Returns(Task.FromResult(42));\n\t\tawait topicService.DeleteTopic(topic, user);\n\t\tawait _topicRepo.Received(1).UpdateReplyCount(topic.TopicID, 42);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteTopicDeletesWithStarter()\n\t{\n\t\tvar user = GetUser();\n\t\tvar topic = new Topic { TopicID = 1, StartedByUserID = user.UserID };\n\t\tvar topicService = GetTopicService();\n\t\tawait topicService.DeleteTopic(topic, user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicDelete, topic, null);\n\t\tawait _topicRepo.Received(1).DeleteTopic(topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteTopicThrowsWithNonMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tvar topicService = GetTopicService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await topicService.UndeleteTopic(topic, user));\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteTopicUndeletesWithMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tawait topicService.UndeleteTopic(topic, user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicUndelete, topic, null);\n\t\tawait _topicRepo.Received(1).UndeleteTopic(topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteTopicUpdatesCounts()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 123 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t_forumService.Get(topic.ForumID).Returns(Task.FromResult(forum));\n\t\tawait topicService.UndeleteTopic(topic, user);\n\t\t_forumService.Received(1).UpdateCounts(forum);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteTopicUpdatesLast()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 123 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t_forumService.Get(topic.ForumID).Returns(Task.FromResult(forum));\n\t\tawait topicService.UndeleteTopic(topic, user);\n\t\tawait _forumService.Received(1).UpdateLast(forum);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteQueuesReindex()\n\t{\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 123 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\t_forumService.Get(topic.ForumID).Returns(Task.FromResult(forum));\n\t\tSearchIndexPayload payload = null;\n\t\tawait _searchIndexQueueRepo.Enqueue(Arg.Do<SearchIndexPayload>(x => payload = x));\n\t\t_tenantService.GetTenant().Returns(\"t\");\n\n\t\tawait topicService.UndeleteTopic(topic, user);\n\n\t\tAssert.Equal(topic.TopicID, payload.TopicID);\n\t\tAssert.Equal(\"t\", payload.TenantID);\n\t\tAssert.False(payload.IsForRemoval);\n\t}\n\n\t[Fact]\n\tpublic async Task UndeleteTopicUpdatesReplyCount()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\t_postRepo.GetReplyCount(topic.TopicID, false).Returns(Task.FromResult(42));\n\t\tawait topicService.UndeleteTopic(topic, user);\n\t\tawait _topicRepo.Received(1).UpdateReplyCount(topic.TopicID, 42);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTopicThrowsWithNonMod()\n\t{\n\t\tvar topic = new Topic { TopicID = 1 };\n\t\tvar user = GetUser();\n\t\tvar topicService = GetTopicService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await topicService.UpdateTitleAndForum(topic, new Forum { ForumID = 2 }, \"blah\", user));\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTopicUpdatesTitleWithMod()\n\t{\n\t\tvar forum = new Forum { ForumID = 2 };\n\t\tvar topic = new Topic { TopicID = 1, ForumID = forum.ForumID };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(new Topic { TopicID = 1, ForumID = 2 }));\n\t\t_topicRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\tawait topicService.UpdateTitleAndForum(topic, forum, \"new title\", user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicRenamed, topic, forum, Arg.Any<string>());\n\t\tawait _topicRepo.Received(1).UpdateTitleAndForum(topic.TopicID, forum.ForumID, \"new title\", \"new-title\");\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTopicQueuesTopicForIndexingWithMod()\n\t{\n\t\tvar forum = new Forum { ForumID = 2 };\n\t\tvar topic = new Topic { TopicID = 1, ForumID = forum.ForumID };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(new Topic { TopicID = 1, ForumID = 2 }));\n\t\t_topicRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\t_tenantService.GetTenant().Returns(\"\");\n\t\tawait topicService.UpdateTitleAndForum(topic, forum, \"new title\", user);\n\t\tawait _searchIndexQueueRepo.Received().Enqueue(Arg.Any<SearchIndexPayload>());\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTopicMovesTopicWithMod()\n\t{\n\t\tvar forum = new Forum { ForumID = 2 };\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 7, Title = String.Empty };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(new Topic { TopicID = 1, ForumID = 3 }));\n\t\t_topicRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\tawait topicService.UpdateTitleAndForum(topic, forum, string.Empty, user);\n\t\tawait _modService.Received(1).LogTopic(user, ModerationType.TopicMoved, topic, forum, Arg.Any<string>());\n\t\tawait _topicRepo.Received(1).UpdateTitleAndForum(topic.TopicID, forum.ForumID, String.Empty, String.Empty);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTopicWithNewTitleChangesUrlNameOnTopicParameter()\n\t{\n\t\tvar forum = new Forum { ForumID = 2 };\n\t\tvar topic = new Topic { TopicID = 1, ForumID = forum.ForumID, UrlName = \"old\" };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\t_topicRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(new Topic { TopicID = 1, ForumID = 2 }));\n\t\tawait topicService.UpdateTitleAndForum(topic, forum, \"new title\", user);\n\t\tAssert.Equal(\"new-title\", topic.UrlName);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTopicMovesUpdatesCountAndLastOnOldForum()\n\t{\n\t\tvar forum = new Forum { ForumID = 2 };\n\t\tvar oldForum = new Forum { ForumID = 3 };\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 7, Title = String.Empty };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(new Topic { TopicID = 1, ForumID = oldForum.ForumID }));\n\t\t_forumService.Get(oldForum.ForumID).Returns(Task.FromResult(oldForum));\n\t\t_topicRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\tawait topicService.UpdateTitleAndForum(topic, forum, String.Empty, user);\n\t\t_forumService.Received(1).UpdateCounts(forum);\n\t\tawait _forumService.Received(1).UpdateLast(forum);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateTopicMovesUpdatesCountAndLastOnNewForum()\n\t{\n\t\tvar forum = new Forum { ForumID = 2 };\n\t\tvar oldForum = new Forum { ForumID = 3 };\n\t\tvar topic = new Topic { TopicID = 1, ForumID = 7, Title = String.Empty };\n\t\tvar user = GetUser();\n\t\tuser.Roles.Add(PermanentRoles.Moderator);\n\t\tvar topicService = GetTopicService();\n\t\t_topicRepo.Get(topic.TopicID).Returns(Task.FromResult(new Topic { TopicID = 1, ForumID = oldForum.ForumID }));\n\t\t_forumService.Get(oldForum.ForumID).Returns(Task.FromResult(oldForum));\n\t\t_topicRepo.GetUrlNamesThatStartWith(Arg.Any<string>()).Returns(Task.FromResult(new List<string>()));\n\t\tawait topicService.UpdateTitleAndForum(topic, forum, String.Empty, user);\n\t\t_forumService.Received(1).UpdateCounts(oldForum);\n\t\tawait _forumService.Received(1).UpdateLast(oldForum);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateLastSetsFieldsFromLastPost()\n\t{\n\t\tvar topic = new Topic { TopicID = 456 };\n\t\tvar post = new Post { PostID = 123, TopicID = topic.TopicID, UserID = 789, Name = \"Dude\", PostTime = new DateTime(2000, 1, 3)};\n\t\tvar service = GetTopicService();\n\t\t_postRepo.GetLastInTopic(post.TopicID).Returns(Task.FromResult(post));\n\t\tawait service.UpdateLast(topic);\n\t\tawait _topicRepo.Received().UpdateLastTimeAndUser(topic.TopicID, post.UserID, post.Name, post.PostTime);\n\t}\n\n\t[Fact]\n\tpublic async Task HardDeleteThrowsIfUserNotAdmin()\n\t{\n\t\tvar user = new User { UserID = 123, Roles = new List<string>() };\n\t\tvar topic = new Topic { TopicID = 45 };\n\t\tvar service = GetTopicService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await service.HardDeleteTopic(topic, user));\n\t}\n\n\t[Fact]\n\tpublic async Task HardDeleteCallsModerationService()\n\t{\n\t\tvar user = new User { UserID = 123, Roles = new List<string> { \"Admin\" } };\n\t\tvar topic = new Topic { TopicID = 45 };\n\t\tvar service = GetTopicService();\n\t\tawait service.HardDeleteTopic(topic, user);\n\t\tawait _modService.Received().LogTopic(user, ModerationType.TopicDeletePermanently, topic, null);\n\t}\n\n\t[Fact]\n\tpublic async Task HardDeleteCallsSearchIndexRepo()\n\t{\n\t\tvar user = new User { UserID = 123, Roles = new List<string> { \"Admin\" } };\n\t\tvar topic = new Topic { TopicID = 45 };\n\t\tvar service = GetTopicService();\n\t\tSearchIndexPayload payload = null;\n\t\tawait _searchIndexQueueRepo.Enqueue(Arg.Do<SearchIndexPayload>(x => payload = x));\n\t\t_tenantService.GetTenant().Returns(\"t\");\n\n\t\tawait service.HardDeleteTopic(topic, user);\n\n\t\tawait _searchIndexQueueRepo.Received().Enqueue(Arg.Any<SearchIndexPayload>());\n\t\tAssert.Equal(topic.TopicID, payload.TopicID);\n\t\tAssert.Equal(\"t\", payload.TenantID);\n\t\tAssert.True(payload.IsForRemoval);\n\t}\n\n\t[Fact]\n\tpublic async Task HardDeleteCallsTopiRepoToDeleteTopic()\n\t{\n\t\tvar user = new User { UserID = 123, Roles = new List<string> { \"Admin\" } };\n\t\tvar topic = new Topic { TopicID = 45 };\n\t\tvar service = GetTopicService();\n\t\tawait service.HardDeleteTopic(topic, user);\n\t\tawait _topicRepo.Received().HardDeleteTopic(topic.TopicID);\n\t}\n\n\t[Fact]\n\tpublic async Task HardDeleteCallsForumServiceToUpdateLastAndCounts()\n\t{\n\t\tvar user = new User { UserID = 123, Roles = new List<string> { \"Admin\" } };\n\t\tvar topic = new Topic { TopicID = 45, ForumID = 67};\n\t\tvar forum = new Forum { ForumID = topic.ForumID };\n\t\tvar service = GetTopicService();\n\t\t_forumService.Get(topic.ForumID).Returns(Task.FromResult(forum));\n\t\tawait service.HardDeleteTopic(topic, user);\n\t\t_forumService.Received().UpdateCounts(forum);\n\t\tawait _forumService.Received().UpdateLast(forum);\n\t}\n\n\t[Fact]\n\tpublic async Task SetAnswerThrowsWhenUserNotTopicStarter()\n\t{\n\t\tvar service = GetTopicService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456, StartedByUserID = 789 };\n\t\tawait Assert.ThrowsAsync<SecurityException>(async () => await service.SetAnswer(user, topic, new Post { PostID = 789 }, \"\", \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task SetAnswerThrowsIfPostIDOfAnswerDoesntExist()\n\t{\n\t\tvar service = GetTopicService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456, StartedByUserID = 123 };\n\t\t_postRepo.Get(Arg.Any<int>()).Returns((Post) null);\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await service.SetAnswer(user, topic, new Post { PostID = 789 }, \"\", \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task SetAnswerThrowsIfPostIsNotPartOfTopic()\n\t{\n\t\tvar service = GetTopicService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456, StartedByUserID = 123 };\n\t\tvar post = new Post { PostID = 789, TopicID = 111 };\n\t\t_postRepo.Get(post.PostID).Returns(Task.FromResult(post));\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(async () => await service.SetAnswer(user, topic, post, \"\", \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task SetAnswerCallsTopicRepoWithUpdatedValue()\n\t{\n\t\tvar service = GetTopicService();\n\t\tvar user = new User { UserID = 123, Name = \"Dude\" };\n\t\tvar topic = new Topic { TopicID = 456, StartedByUserID = 123, Title = \"the title\" };\n\t\tvar post = new Post { PostID = 789, TopicID = topic.TopicID, UserID = 777 };\n\t\t_postRepo.Get(post.PostID).Returns(Task.FromResult(post));\n\n\t\tawait service.SetAnswer(user, topic, post, \"\", \"\");\n\n\t\tawait _topicRepo.Received().UpdateAnswerPostID(topic.TopicID, post.PostID);\n\t\tawait _notificationAdapter.Received().QuestionAnswer(user.Name, topic.Title, post.PostID, post.UserID);\n\t}\n\n\t[Fact]\n\tpublic async Task SetAnswerCallsEventPubWhenThereIsNoPreviousAnswerOnTheTopic()\n\t{\n\t\tvar service = GetTopicService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar answerUser = new User { UserID = 777 };\n\t\tvar topic = new Topic { TopicID = 456, StartedByUserID = user.UserID, AnswerPostID = null};\n\t\tvar post = new Post { PostID = 789, TopicID = topic.TopicID, UserID = answerUser.UserID};\n\t\t_postRepo.Get(post.PostID).Returns(Task.FromResult(post));\n\t\t_userRepo.GetUser(answerUser.UserID).Returns(Task.FromResult(answerUser));\n\t\tawait service.SetAnswer(user, topic, post, \"\", \"\");\n\t\tawait _eventPublisher.Received().ProcessEvent(Arg.Any<string>(), answerUser, EventDefinitionService.StaticEventIDs.QuestionAnswered, false);\n\t}\n\n\t[Fact]\n\tpublic async Task SetAnswerDoesNotCallEventPubWhenTheAnswerUserDoesNotExist()\n\t{\n\t\tvar service = GetTopicService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456, StartedByUserID = user.UserID, AnswerPostID = null };\n\t\tvar post = new Post { PostID = 789, TopicID = topic.TopicID, UserID = 777 };\n\t\t_postRepo.Get(post.PostID).Returns(Task.FromResult(post));\n\t\t_userRepo.GetUser(Arg.Any<int>()).Returns((User)null);\n\t\tawait service.SetAnswer(user, topic, post, \"\", \"\");\n\t\tawait _eventPublisher.DidNotReceive().ProcessEvent(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>());\n\t}\n\n\t[Fact]\n\tpublic async Task SetAnswerDoesNotCallEventPubWhenTheTopicAlreadyHasAnAnswer()\n\t{\n\t\tvar service = GetTopicService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar answerUser = new User { UserID = 777 };\n\t\tvar topic = new Topic { TopicID = 456, StartedByUserID = user.UserID, AnswerPostID = 666 };\n\t\tvar post = new Post { PostID = 789, TopicID = topic.TopicID, UserID = answerUser.UserID };\n\t\t_postRepo.Get(post.PostID).Returns(Task.FromResult(post));\n\t\t_userRepo.GetUser(answerUser.UserID).Returns(Task.FromResult(answerUser));\n\t\tawait service.SetAnswer(user, topic, post, \"\", \"\");\n\t\tawait _eventPublisher.DidNotReceive().ProcessEvent(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>());\n\t}\n\n\t[Fact]\n\tpublic async Task SetAnswerDoesNotCallEventPubWhenTopicUserIDIsSameAsAnswerUserID()\n\t{\n\t\tvar service = GetTopicService();\n\t\tvar user = new User { UserID = 123 };\n\t\tvar topic = new Topic { TopicID = 456, StartedByUserID = user.UserID, AnswerPostID = null };\n\t\tvar post = new Post { PostID = 789, TopicID = topic.TopicID, UserID = user.UserID };\n\t\t_postRepo.Get(post.PostID).Returns(Task.FromResult(post));\n\t\t_userRepo.GetUser(user.UserID).Returns(Task.FromResult(user));\n\t\tawait service.SetAnswer(user, topic, post, \"\", \"\");\n\t\tawait _eventPublisher.DidNotReceive().ProcessEvent(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>());\n\t}\n\n\t[Fact]\n\tpublic async Task CloseAgedTopicsDoesNothingWhenSettingIsOff()\n\t{\n\t\tvar service = GetTopicService();\n\t\t_settingsManager.Current.IsClosingAgedTopics.Returns(false);\n\n\t\tawait service.CloseAgedTopics();\n\n\t\tawait _topicRepo.DidNotReceive().CloseTopicsOlderThan(Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task CloseAgedTopicsCallsRepoWhenSettingIsOn()\n\t{\n\t\tvar service = GetTopicService();\n\t\t_settingsManager.Current.IsClosingAgedTopics.Returns(true);\n\t\t_topicRepo.CloseTopicsOlderThan(Arg.Any<DateTime>()).Returns(new List<int>());\n\n\t\tawait service.CloseAgedTopics();\n\n\t\tawait _topicRepo.Received().CloseTopicsOlderThan(Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task CloseAgedTopicsLogsTopicModeration()\n\t{\n\t\tvar service = GetTopicService();\n\t\t_settingsManager.Current.IsClosingAgedTopics.Returns(true);\n\t\t_topicRepo.CloseTopicsOlderThan(Arg.Any<DateTime>()).Returns(new List<int>{1,2,3});\n\n\t\tawait service.CloseAgedTopics();\n\n\t\tawait _topicRepo.Received().CloseTopicsOlderThan(Arg.Any<DateTime>());\n\t\tawait _modService.Received().LogTopic(ModerationType.TopicCloseAuto, 1);\n\t\tawait _modService.Received().LogTopic(ModerationType.TopicCloseAuto, 2);\n\t\tawait _modService.Received().LogTopic(ModerationType.TopicCloseAuto, 3);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/TopicViewLogServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class TopicViewLogServiceTests\n{\n\tprivate TopicViewLogService GetService()\n\t{\n\t\t_config = Substitute.For<IConfig>();\n\t\t_topicViewLogRepo = Substitute.For<ITopicViewLogRepository>();\n\t\treturn new TopicViewLogService(_config, _topicViewLogRepo);\n\t}\n\n\tprivate IConfig _config;\n\tprivate ITopicViewLogRepository _topicViewLogRepo;\n\n\t[Fact]\n\tpublic async Task LogViewDoesNotCallRepoWhenConfigIsFalse()\n\t{\n\t\tvar service = GetService();\n\t\t_config.LogTopicViews.Returns(false);\n\n\t\tawait service.LogView(123, 456);\n\n\t\tawait _topicViewLogRepo.DidNotReceive().Log(Arg.Any<int?>(), Arg.Any<int>(), Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task LogViewDoesCallsRepoWhenConfigIsTrue()\n\t{\n\t\tvar service = GetService();\n\t\t_config.LogTopicViews.Returns(true);\n\n\t\tawait service.LogView(123, 456);\n\n\t\tawait _topicViewLogRepo.Received().Log(123, 456, Arg.Any<DateTime>());\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/UserEmailReconcilerTests.cs",
    "content": "namespace PopForums.Test.Services;\n\npublic class UserEmailReconcilerTests\n{\n\tprivate IUserRepository _userRepo;\n\n\tprivate UserEmailReconciler GetService()\n\t{\n\t\t_userRepo = Substitute.For<IUserRepository>();\n\t\treturn new UserEmailReconciler(_userRepo);\n\t}\n\n\tpublic class GetUniqueEmail : UserEmailReconcilerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task UnmatchedReturnsSameEmail()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar email = \"a@b.com\";\n\t\t\t_userRepo.GetUserByEmail(email).Returns((User)null);\n\n\t\t\tvar result = await service.GetUniqueEmail(email, \"12345\");\n\t\t\t\n\t\t\tAssert.Equal(email, result);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task MatchedReturnsUniqueEmail()\n\t\t{\n\t\t\tvar service = GetService();\n\t\t\tvar email = \"a@b.com\";\n\t\t\tvar user = new User { Email = email };\n\t\t\t_userRepo.GetUserByEmail(email).Returns(Task.FromResult(user));\n\n\t\t\tvar result = await service.GetUniqueEmail(email, \"12345\");\n\t\t\t\n\t\t\tAssert.Equal(\"a-at-b.com@12345.example.com\", result);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/UserNameReconcilerTests.cs",
    "content": "namespace PopForums.Test.Services;\n\npublic class UserNameReconcilerTests\n{\n\tprivate IUserRepository _userRepo;\n\n\tprivate UserNameReconciler GetService()\n\t{\n\t\t_userRepo = Substitute.For<IUserRepository>();\n\t\treturn new UserNameReconciler(_userRepo);\n\t}\n\n\tpublic class GetUniqueNameForNewUser : UserNameReconcilerTests\n\t{\n\t\t[Fact]\n\t\tpublic async Task NoMatchesReturnsSameName()\n\t\t{\n\t\t\tvar name = \"Jeff P\";\n\t\t\tvar service = GetService();\n\t\t\t_userRepo.GetUserNamesThatStartWith(name).Returns(new string[]{});\n\n\t\t\tvar result = await service.GetUniqueNameForUser(name);\n\t\t\t\n\t\t\tAssert.Equal(name, result);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task OneMatchesReturnsAppendedName()\n\t\t{\n\t\t\tvar name = \"Jeff P\";\n\t\t\tvar service = GetService();\n\t\t\t_userRepo.GetUserNamesThatStartWith(name).Returns(new []{ name });\n\n\t\t\tvar result = await service.GetUniqueNameForUser(name);\n\t\t\t\n\t\t\tAssert.Equal(\"Jeff P-2\", result);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ThreeMatchesReturnsAppendedName()\n\t\t{\n\t\t\tvar name = \"Jeff P\";\n\t\t\tvar service = GetService();\n\t\t\t_userRepo.GetUserNamesThatStartWith(name).Returns(new []{ \"Jeff P\", \"Jeff P-2\", \"Jeff P-3\" });\n\n\t\t\tvar result = await service.GetUniqueNameForUser(name);\n\t\t\t\n\t\t\tAssert.Equal(\"Jeff P-4\", result);\n\t\t}\n\t\t\n\t\t[Fact]\n\t\tpublic async Task ThreeMatchesWithOneExtraReturnsAppendedName()\n\t\t{\n\t\t\tvar name = \"Jeff P\";\n\t\t\tvar service = GetService();\n\t\t\t_userRepo.GetUserNamesThatStartWith(name).Returns(new []{ \"Jeff P\", \"Jeff Peterson\", \"Jeff P-2\" });\n\n\t\t\tvar result = await service.GetUniqueNameForUser(name);\n\t\t\t\n\t\t\tAssert.Equal(\"Jeff P-3\", result);\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/UserServiceTests.cs",
    "content": "﻿namespace PopForums.Test.Services;\n\npublic class UserServiceTests\n{\n\tprivate IUserRepository _mockUserRepo;\n\tprivate IRoleRepository _mockRoleRepo;\n\tprivate IProfileRepository _mockProfileRepo;\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate IUserAvatarRepository _mockUserAvatarRepo;\n\tprivate IUserImageRepository _mockUserImageRepo;\n\tprivate ISecurityLogService _mockSecurityLogService;\n\tprivate ITextParsingService _mockTextParser;\n\tprivate IBanRepository _mockBanRepo;\n\tprivate IForgotPasswordMailer _mockForgotMailer;\n\tprivate IImageService _mockImageService;\n\tprivate IConfig _config;\n\n\tprivate UserService GetMockedUserService()\n\t{\n\t\t_mockUserRepo = Substitute.For<IUserRepository>();\n\t\t_mockRoleRepo = Substitute.For<IRoleRepository>();\n\t\t_mockProfileRepo = Substitute.For<IProfileRepository>();\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_mockUserAvatarRepo = Substitute.For<IUserAvatarRepository>();\n\t\t_mockUserImageRepo = Substitute.For<IUserImageRepository>();\n\t\t_mockSecurityLogService = Substitute.For<ISecurityLogService>();\n\t\t_mockTextParser = Substitute.For<ITextParsingService>();\n\t\t_mockBanRepo = Substitute.For<IBanRepository>();\n\t\t_mockForgotMailer = Substitute.For<IForgotPasswordMailer>();\n\t\t_mockImageService = Substitute.For<IImageService>();\n\t\t_config = Substitute.For<IConfig>();\n\t\t_mockRoleRepo.GetUserRoles(Arg.Any<int>()).Returns(Task.FromResult(new List<string>()));\n\t\treturn new UserService(_mockUserRepo, _mockRoleRepo, _mockProfileRepo, _mockSettingsManager, _mockUserAvatarRepo, _mockUserImageRepo, _mockSecurityLogService, _mockTextParser, _mockBanRepo, _mockForgotMailer, _mockImageService, _config);\n\t}\n\n\t[Fact]\n\tpublic async Task SetPassword()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar user = GetDummyUser(\"jeff\", \"a@b.com\");\n\t\tvar salt = Guid.NewGuid();\n\t\tawait _mockUserRepo.SetHashedPassword(user, Arg.Any<string>(), Arg.Do<Guid>(x => salt = x));\n\n\t\tawait userService.SetPassword(user, \"fred\", String.Empty, user);\n\n\t\tvar hashedPassword = \"fred\".GetSHA256Hash(salt);\n\t\tawait _mockUserRepo.Received().SetHashedPassword(user, hashedPassword, salt);\n\t}\n\n\t[Fact]\n\tpublic async Task CheckPassword()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetHashedPasswordByEmail(string.Empty).Returns(Task.FromResult(Tuple.Create(\"0M/C5TGbgs3HGjOHPoJsk9fuETY/iskcT6Oiz80ihuU=\", (Guid?)null)));\n\t\t\n\t\tvar result = await userService.CheckPassword(String.Empty, \"fred\");\n\n\t\tAssert.True(result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task CheckPasswordFail()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetHashedPasswordByEmail(string.Empty).Returns(Task.FromResult(Tuple.Create(\"VwqQv7+MfqtdxdTiaDLVsQ==\", (Guid?)null)));\n\n\t\tvar result = await userService.CheckPassword(String.Empty, \"fsdfsdfsdfsdf\");\n\n\t\tAssert.False(result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task CheckPasswordHasSalt()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = Guid.NewGuid();\n\t\tvar hashedPassword = \"fred\".GetSHA256Hash(salt.Value);\n\t\t_mockUserRepo.GetHashedPasswordByEmail(string.Empty).Returns(Task.FromResult(Tuple.Create(hashedPassword, salt)));\n\n\t\tvar result = await userService.CheckPassword(String.Empty, \"fred\");\n\t\tAssert.True(result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task CheckPasswordHasSaltFail()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = Guid.NewGuid();\n\t\tvar hashedPassword = \"fred\".GetSHA256Hash(salt.Value);\n\t\t_mockUserRepo.GetHashedPasswordByEmail(string.Empty).Returns(Task.FromResult(Tuple.Create(hashedPassword, salt)));\n\n\t\t var result = await userService.CheckPassword(String.Empty, \"dsfsdfsdfsdf\");\n\t\tAssert.False(result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task CheckPasswordPassesWithoutSaltOnMD5Fallback()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = null;\n\t\tvar hashedPassword = \"fred\".GetMD5Hash();\n\t\t_mockUserRepo.GetHashedPasswordByEmail(string.Empty).Returns(Task.FromResult(Tuple.Create(hashedPassword, salt)));\n\n\t\tvar result = await userService.CheckPassword(String.Empty, \"fred\");\n\t\tAssert.True(result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task  CheckPasswordPassesWithSaltOnMD5Fallback()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = Guid.NewGuid();\n\t\tvar hashedPassword = \"fred\".GetMD5Hash(salt.Value);\n\t\t_mockUserRepo.GetHashedPasswordByEmail(string.Empty).Returns(Task.FromResult(Tuple.Create(hashedPassword, salt)));\n\n\t\tvar result = await userService.CheckPassword(String.Empty, \"fred\");\n\t\tAssert.True(result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task CheckPasswordFailsOnMD5FallbackNoMatch()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = Guid.NewGuid();\n\t\tvar hashedPassword = \"fred\".GetMD5Hash(salt.Value);\n\t\t_mockUserRepo.GetHashedPasswordByEmail(string.Empty).Returns(Task.FromResult(Tuple.Create(hashedPassword, salt)));\n\n\t\tvar result = await userService.CheckPassword(String.Empty, \"blah\");\n\t\tAssert.False(result.Item1);\n\t}\n\n\t[Fact]\n\tpublic async Task CheckPasswordFailsMD5FallbackDoesNotCallUpdate()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = Guid.NewGuid();\n\t\tvar email = \"a@b.com\";\n\t\tvar user = new User { Email = email };\n\t\tvar hashedPassword = \"fred\".GetMD5Hash(salt.Value);\n\t\t_mockUserRepo.GetHashedPasswordByEmail(email).Returns(Task.FromResult(Tuple.Create(hashedPassword, salt)));\n\t\t_mockUserRepo.GetUserByEmail(email).Returns(Task.FromResult(user));\n\n\t\tvar result = await userService.CheckPassword(email, \"blah\");\n\t\tAssert.False(result.Item1);\n\t\tawait _mockUserRepo.DidNotReceive().SetHashedPassword(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<Guid>());\n\t}\n\n\t[Fact]\n\tpublic async Task CheckPasswordPassesWithSaltOnMD5FallbackCallsUpdate()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = Guid.NewGuid();\n\t\tvar email = \"a@b.com\";\n\t\tvar user = new User {Email = email};\n\t\tvar hashedPassword = \"fred\".GetMD5Hash(salt.Value);\n\t\t_mockUserRepo.GetHashedPasswordByEmail(email).Returns(Task.FromResult(Tuple.Create(hashedPassword, salt)));\n\t\t_mockUserRepo.GetUserByEmail(email).Returns(Task.FromResult(user));\n\n\t\tvar result = await userService.CheckPassword(email, \"fred\");\n\t\tAssert.True(result.Item1);\n\t\tawait _mockUserRepo.Received().SetHashedPassword(user, Arg.Any<string>(), Arg.Any<Guid>());\n\t}\n\n\t[Fact]\n\tpublic async Task GetUser()\n\t{\n\t\tconst int id = 1;\n\t\tconst string name = \"Jeff\";\n\t\tconst string email = \"a@b.com\";\n\t\tvar roles = new List<string> {\"blah\", PermanentRoles.Admin};\n\t\tvar userService = GetMockedUserService();\n\t\tvar dummyUser = GetDummyUser(name, email);\n\t\t_mockUserRepo.GetUser(id).Returns(Task.FromResult(dummyUser));\n\t\t_mockRoleRepo.GetUserRoles(id).Returns(Task.FromResult(roles));\n\t\tvar user = await userService.GetUser(id);\n\t\tAssert.Same(dummyUser, user);\n\t}\n\n\t[Fact]\n\tpublic async Task GetUserFail()\n\t{\n\t\tconst int id = 1;\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetUser(Arg.Is<int>(i => i != 1)).Returns(Task.FromResult(GetDummyUser(\"\", \"\")));\n\t\t_mockUserRepo.GetUser(Arg.Is<int>(i => i == 1)).Returns((User)null);\n\t\tvar user = await userService.GetUser(id);\n\t\tAssert.Null(user);\n\t}\n\n\t[Fact]\n\tpublic async Task GetUserByName()\n\t{\n\t\tconst string name = \"Jeff\";\n\t\tconst string email = \"a@b.com\";\n\t\tvar roles = new List<string> { \"blah\", PermanentRoles.Admin };\n\t\tvar dummyUser = GetDummyUser(name, email);\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByName(name.ToLower()).Returns(Task.FromResult(dummyUser));\n\t\t_mockRoleRepo.GetUserRoles(dummyUser.UserID).Returns(Task.FromResult(roles));\n\t\tvar user = await userService.GetUserByName(name);\n\t\tAssert.Same(dummyUser, user);\n\t}\n\n\t[Fact]\n\tpublic async Task GetUserByNameFail()\n\t{\n\t\tconst string name = \"Jeff\";\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByName(Arg.Is<string>(i => i != name.ToLower())).Returns(Task.FromResult(GetDummyUser(name, \"\")));\n\t\t_mockUserRepo.GetUserByName(Arg.Is<string>(i => i == name.ToLower())).Returns((User)null);\n\t\tvar user = await userService.GetUserByName(name);\n\t\tAssert.Null(user);\n\t}\n\n\t[Fact]\n\tpublic async Task GetUserByNameReturnsNullWithNullOrEmptyName()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tAssert.Null(await userService.GetUserByName(\"\"));\n\t\tAssert.Null(await userService.GetUserByName(null));\n\t}\n\n\t[Fact]\n\tpublic async Task GetUserByEmail()\n\t{\n\t\tconst string name = \"Jeff\";\n\t\tconst string email = \"a@b.com\";\n\t\tvar roles = new List<string> { \"blah\", PermanentRoles.Admin };\n\t\tvar dummyUser = GetDummyUser(name, email);\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(email).Returns(Task.FromResult(dummyUser));\n\t\t_mockRoleRepo.GetUserRoles(dummyUser.UserID).Returns(Task.FromResult(roles));\n\t\tvar user = await userService.GetUserByEmail(email);\n\t\tAssert.Same(dummyUser, user);\n\t}\n\n\t[Fact]\n\tpublic async Task GetUserByEmailFail()\n\t{\n\t\tconst string email = \"a@b.com\";\n\t\tvar userManager = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(Arg.Is<string>(i => i != email)).Returns(Task.FromResult(GetDummyUser(\"\", email)));\n\t\t_mockUserRepo.GetUserByEmail(Arg.Is<string>(i => i == email)).Returns((User)null);\n\t\tvar user = await userManager.GetUserByEmail(email);\n\t\tAssert.Null(user);\n\t}\n\n\tpublic static User GetDummyUser(string name, string email)\n\t{\n\t\treturn new User { UserID = 1, Name = name, Email = email, IsApproved = true, AuthorizationKey = new Guid()};\n\t}\n\n\t[Fact]\n\tpublic async Task NameIsInUse()\n\t{\n\t\tconst string name = \"jeff\";\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByName(Arg.Is(name)).Returns(Task.FromResult(GetDummyUser(name, \"a@b.com\")));\n\t\t\n\t\tAssert.True(await userService.IsNameInUse(name));\n\t\tawait _mockUserRepo.Received(1).GetUserByName(name);\n\t\tAssert.False(await userService.IsNameInUse(\"notjeff\"));\n\t\tAssert.True(await userService.IsNameInUse(name.ToUpper()));\n\t}\n\n\t[Fact]\n\tpublic async Task EmailIsInUse()\n\t{\n\t\tconst string email = \"a@b.com\";\n\t\tvar userManager = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(Arg.Is(email)).Returns(Task.FromResult(GetDummyUser(\"jeff\", email)));\n\t\tAssert.True(await userManager.IsEmailInUse(email));\n\t\tawait _mockUserRepo.Received(1).GetUserByEmail(email);\n\t\tAssert.False(await userManager.IsEmailInUse(\"nota@b.com\"));\n\t\tAssert.True(await userManager.IsEmailInUse(email.ToUpper()));\n\t}\n\n\t[Fact]\n\tpublic async Task EmailInUserByAnotherTrue()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar user = GetDummyUser(\"jeff\", \"a@b.com\");\n\t\t_mockUserRepo.GetUserByEmail(\"c@d.com\").Returns(Task.FromResult(new User { UserID = 123 }));\n\t\tvar result = await userService.IsEmailInUseByDifferentUser(user, \"c@d.com\");\n\t\tawait _mockUserRepo.Received().GetUserByEmail(\"c@d.com\");\n\t\tAssert.True(result);\n\t}\n\n\t[Fact]\n\tpublic async Task EmailInUserByAnotherFalseBecauseSameUser()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar user = GetDummyUser(\"jeff\", \"a@b.com\");\n\t\t_mockUserRepo.GetUserByEmail(\"a@b.com\").Returns(Task.FromResult(user));\n\t\tvar result = await userService.IsEmailInUseByDifferentUser(user, \"a@b.com\");\n\t\tawait _mockUserRepo.Received().GetUserByEmail(\"a@b.com\");\n\t\tAssert.False(result);\n\t}\n\n\t[Fact]\n\tpublic async Task EmailInUserByAnotherFalseBecauseNoUser()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar user = GetDummyUser(\"jeff\", \"a@b.com\");\n\t\t_mockUserRepo.GetUserByEmail(\"c@d.com\").Returns((User)null);\n\t\tvar result = await userService.IsEmailInUseByDifferentUser(user, \"c@d.com\");\n\t\tawait _mockUserRepo.Received().GetUserByEmail(\"c@d.com\");\n\t\tAssert.False(result);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateUser()\n\t{\n\t\tconst string name = \"jeff\";\n\t\tconst string nameCensor = \"jeffcensor\";\n\t\tconst string email = \"a@b.com\";\n\t\tconst string password = \"fred\";\n\t\tconst string ip = \"127.0.0.1\";\n\t\tvar userService = GetMockedUserService();\n\t\tvar dummyUser = GetDummyUser(nameCensor, email);\n\t\t_mockUserRepo.CreateUser(nameCensor, email, Arg.Any<DateTime>(), true, Arg.Any<string>(), Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(Task.FromResult(dummyUser));\n\t\t_mockTextParser.Censor(name).Returns(nameCensor);\n\t\tvar user = await userService.CreateUser(name, email, password, true, ip);\n\t\tAssert.Equal(dummyUser.Name, user.Name);\n\t\tAssert.Equal(dummyUser.Email, user.Email);\n\t\t_mockTextParser.Received().Censor(name);\n\t\tawait _mockUserRepo.Received().CreateUser(nameCensor, email, Arg.Any<DateTime>(), true, Arg.Any<string>(), Arg.Any<Guid>(), Arg.Any<Guid>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user, ip, String.Empty, SecurityLogType.UserCreated);\n\t}\n\n\n\t[Fact]\n\tpublic async Task CreateUserFromSignup()\n\t{\n\t\tconst string name = \"jeff\";\n\t\tconst string nameCensor = \"jeffcensor\";\n\t\tconst string email = \"a@b.com\";\n\t\tconst string password = \"fred\";\n\t\tconst string ip = \"127.0.0.1\";\n\t\tvar userManager = GetMockedUserService();\n\t\tvar dummyUser = GetDummyUser(nameCensor, email);\n\t\t_mockUserRepo.CreateUser(nameCensor, email, Arg.Any<DateTime>(), true, Arg.Any<string>(), Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(Task.FromResult(dummyUser));\n\t\t_mockTextParser.Censor(name).Returns(nameCensor);\n\t\tvar settings = new Settings();\n\t\t_mockSettingsManager.Current.Returns(settings);\n\t\tvar signupData = new SignupData {Email = email, Name = name, Password = password};\n\t\tvar user = await userManager.CreateUserWithProfile(signupData, ip);\n\t\tawait _mockUserRepo.Received().CreateUser(nameCensor, email, Arg.Any<DateTime>(), true, Arg.Any<string>(), Arg.Any<Guid>(), Arg.Any<Guid>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user, ip, String.Empty, SecurityLogType.UserCreated);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateInvalidEmail()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\t_mockTextParser.Censor(Arg.Any<string>()).Returns(\"blah\");\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await userService.CreateUser(\"\", \"a b@oihfwe\", \"\", true, \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateUsedName()\n\t{\n\t\tconst string usedName = \"jeff\";\n\t\tconst string email = \"a@b.com\";\n\t\tvar userService = GetMockedUserService();\n\t\t_mockTextParser.Censor(\"jeff\").Returns(\"jeff\");\n\t\t_mockTextParser.Censor(\"anynamejeff\").Returns(\"anynamejeff\");\n\t\t_mockUserRepo.GetUserByName(Arg.Is(usedName)).Returns(Task.FromResult(GetDummyUser(usedName, email)));\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await userService.CreateUser(usedName, email, \"\", true, \"\"));\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await userService.CreateUser(usedName.ToUpper(), email, \"\", true, \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateNameNull()\n\t{\n\t\tvar userManager = GetMockedUserService();\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await userManager.CreateUser(null, \"a@b.com\", \"\", true, \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateNameEmpty()\n\t{\n\t\tvar userManager = GetMockedUserService();\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await userManager.CreateUser(String.Empty, \"a@b.com\", \"\", true, \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateUsedEmail()\n\t{\n\t\tconst string usedEmail = \"a@b.com\";\n\t\tvar userService = GetMockedUserService();\n\t\t_mockTextParser.Censor(Arg.Any<string>()).Returns(\"blah\");\n\t\t_mockUserRepo.GetUserByEmail(Arg.Is(usedEmail)).Returns(Task.FromResult(GetDummyUser(\"jeff\", usedEmail)));\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await userService.CreateUser(\"\", usedEmail, \"\", true, \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateEmailBanned()\n\t{\n\t\tconst string bannedEmail = \"a@b.com\";\n\t\tvar userService = GetMockedUserService();\n\t\t_mockTextParser.Censor(Arg.Any<string>()).Returns(\"blah\");\n\t\t_mockBanRepo.EmailIsBanned(bannedEmail).Returns(Task.FromResult(true));\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await userService.CreateUser(\"name\", bannedEmail, \"\", true, \"\"));\n\t}\n\n\t[Fact]\n\tpublic async Task CreateIPBanned()\n\t{\n\t\tconst string bannedIP = \"1.2.3.4\";\n\t\tvar userManager = GetMockedUserService();\n\t\t_mockTextParser.Censor(Arg.Any<string>()).Returns(\"blah\");\n\t\t_mockBanRepo.IPIsBanned(bannedIP).Returns(Task.FromResult(true));\n\t\tawait Assert.ThrowsAsync<Exception>(async () => await userManager.CreateUser(\"\", \"a@b.com\", \"\", true, bannedIP));\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateLastActivityDate()\n\t{\n\t\tvar userManager = GetMockedUserService();\n\t\tvar user = UserTest.GetTestUser();\n\t\tawait userManager.UpdateLastActivityDate(user);\n\t\tawait _mockUserRepo.Received().UpdateLastActivityDate(user, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task ChangeEmailSuccess()\n\t{\n\t\tconst string oldName = \"Jeff\";\n\t\tconst string oldEmail = \"a@b.com\";\n\t\tconst string newEmail = \"c@d.com\";\n\n\t\tvar userManager = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(oldEmail).Returns(Task.FromResult(GetDummyUser(oldName, oldEmail)));\n\t\t_mockUserRepo.GetUserByEmail(newEmail).Returns((User)null);\n\t\t_mockSettingsManager.Current.IsNewUserApproved.Returns(false);\n\t\tvar targetUser = GetDummyUser(oldName, oldEmail);\n\t\tvar user = new User { UserID = 34243 };\n\t\tawait userManager.ChangeEmail(targetUser, newEmail, user, \"123\");\n\t\tawait _mockUserRepo.Received(1).ChangeEmail(targetUser, newEmail);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(user, targetUser, \"123\", \"Old: a@b.com, New: c@d.com\", SecurityLogType.EmailChange);\n\t}\n\n\t[Fact]\n\tpublic async Task ChangeEmailAlreadyInUse()\n\t{\n\t\tconst string oldName = \"Jeff\";\n\t\tconst string oldEmail = \"a@b.com\";\n\t\tconst string newEmail = \"c@d.com\";\n\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(oldEmail).Returns(Task.FromResult(GetDummyUser(oldName, oldEmail)));\n\t\t_mockUserRepo.GetUserByEmail(newEmail).Returns(Task.FromResult(GetDummyUser(\"Diana\", newEmail)));\n\t\t_mockSettingsManager.Current.IsNewUserApproved.Returns(true);\n\t\tvar user = GetDummyUser(oldName, oldEmail);\n\t\tawait Assert.ThrowsAsync<Exception>(() => userService.ChangeEmail(user, newEmail, new User(), string.Empty));\n\t\tawait _mockUserRepo.DidNotReceive().ChangeEmail(Arg.Any<User>(), Arg.Any<string>());\n\t}\n\n\t[Fact]\n\tpublic async Task ChangeEmailBad()\n\t{\n\t\tconst string badEmail = \"a b @ c\";\n\t\tvar userManager = GetMockedUserService();\n\t\t_mockSettingsManager.Current.IsNewUserApproved.Returns(true);\n\t\tvar user = GetDummyUser(\"\", \"\");\n\t\tawait Assert.ThrowsAsync<Exception>(() => userManager.ChangeEmail(user, badEmail, new User(), String.Empty));\n\t\tawait _mockUserRepo.DidNotReceive().ChangeEmail(Arg.Any<User>(), Arg.Any<string>());\n\t}\n\n\t[Fact]\n\tpublic async Task ChangeEmailMapsIsApprovedFromSettingsToUserRepoCall()\n\t{\n\t\tconst string oldName = \"Jeff\";\n\t\tconst string oldEmail = \"a@b.com\";\n\t\tconst string newEmail = \"c@d.com\";\n\n\t\tvar userManager = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(oldEmail).Returns(Task.FromResult(GetDummyUser(oldName, oldEmail)));\n\t\t_mockUserRepo.GetUserByEmail(newEmail).Returns((User)null);\n\t\t_mockSettingsManager.Current.IsNewUserApproved.Returns(true);\n\t\tvar targetUser = GetDummyUser(oldName, oldEmail);\n\t\tvar user = new User { UserID = 34243 };\n\t\tawait userManager.ChangeEmail(targetUser, newEmail, user, \"123\");\n\t\tawait _mockUserRepo.Received().UpdateIsApproved(targetUser, true);\n\t}\n\n\t[Fact]\n\tpublic async Task ChangeNameSuccess()\n\t{\n\t\tconst string oldName = \"Jeff\";\n\t\tconst string oldEmail = \"a@b.com\";\n\t\tconst string newName = \"Diana\";\n\n\t\tvar userManager = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByName(oldName).Returns(Task.FromResult(GetDummyUser(oldName, oldEmail)));\n\t\t_mockUserRepo.GetUserByName(newName).Returns((User)null);\n\t\tvar targetUser = GetDummyUser(oldName, oldEmail);\n\t\tvar user = new User { UserID = 1234531 };\n\t\tawait userManager.ChangeName(targetUser, newName, user, \"123\");\n\t\tawait _mockUserRepo.Received().ChangeName(targetUser, newName);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(user, targetUser, \"123\", \"Old: Jeff, New: Diana\", SecurityLogType.NameChange);\n\t}\n\n\t[Fact]\n\tpublic async Task ChangeNameFailUsed()\n\t{\n\t\tconst string oldName = \"Jeff\";\n\t\tconst string oldEmail = \"a@b.com\";\n\t\tconst string newName = \"Diana\";\n\n\t\tvar userService = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByName(oldName.ToLower()).Returns(Task.FromResult(GetDummyUser(oldName, oldEmail)));\n\t\t_mockUserRepo.GetUserByName(newName.ToLower()).Returns(Task.FromResult(GetDummyUser(newName, oldEmail)));\n\t\tvar user = GetDummyUser(oldName, oldEmail);\n\t\tawait Assert.ThrowsAsync<Exception>(() => userService.ChangeName(user, newName, new User(), string.Empty));\n\t\tawait _mockUserRepo.DidNotReceive().ChangeName(Arg.Any<User>(), Arg.Any<string>());\n\t}\n\n\t[Fact]\n\tpublic async Task ChangeNameNull()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar user = GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\tawait Assert.ThrowsAsync<Exception>(() => userService.ChangeName(user, null, new User(), string.Empty));\n\t\tawait _mockUserRepo.DidNotReceive().ChangeName(Arg.Any<User>(), Arg.Any<string>());\n\t}\n\n\t[Fact]\n\tpublic async Task ChangeNameEmpty()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar user = GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\t\n\t\tawait Assert.ThrowsAsync<Exception>(() => userService.ChangeName(user, String.Empty, new User(), string.Empty));\n\t\tawait _mockUserRepo.DidNotReceive().ChangeName(Arg.Any<User>(), Arg.Any<string>());\n\t}\n\n\t[Fact]\n\tpublic async Task Logout()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar user = UserTest.GetTestUser();\n\t\tawait userService.Logout(user, \"123\");\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user, \"123\", String.Empty, SecurityLogType.Logout);\n\t}\n\n\t[Fact]\n\tpublic async Task LoginSuccess()\n\t{\n\t\tconst string email = \"a@b.com\";\n\t\tconst string password = \"fred\";\n\t\tconst string ip = \"1.1.1.1\";\n\t\tvar user = UserTest.GetTestUser();\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = Guid.NewGuid();\n\t\tvar saltedHash = password.GetSHA256Hash(salt.Value);\n\t\t_mockUserRepo.GetHashedPasswordByEmail(email).Returns(Task.FromResult(Tuple.Create(saltedHash, salt)));\n\t\t_mockUserRepo.GetUserByEmail(email).Returns(Task.FromResult(user));\n\n\t\tvar (result, _) = await userService.Login(email, password, ip);\n\n\t\tAssert.True(result);\n\t\tawait _mockUserRepo.Received().UpdateLastLoginDate(user, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user, ip, \"\", SecurityLogType.Login);\n\t\tawait _mockUserRepo.DidNotReceive().SetHashedPassword(user, Arg.Any<string>(), Arg.Any<Guid>());\n\t}\n\n\t[Fact]\n\tpublic async Task LoginSuccessNoSalt()\n\t{\n\t\tconst string email = \"a@b.com\";\n\t\tconst string password = \"fred\";\n\t\tconst string ip = \"1.1.1.1\";\n\t\tvar user = UserTest.GetTestUser();\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = Guid.NewGuid();\n\t\tGuid? nosalt = null;\n\t\t_mockUserRepo.GetHashedPasswordByEmail(email).Returns(Tuple.Create(password.GetSHA256Hash(), nosalt));\n\t\t_mockUserRepo.GetUserByEmail(email).Returns(Task.FromResult(user));\n\t\tawait _mockUserRepo.SetHashedPassword(user, Arg.Any<string>(), Arg.Do<Guid>(x => salt = x));\n\n\t\tvar (result, _) = await userService.Login(email, password, ip);\n\n\t\tAssert.True(result);\n\t\tawait _mockUserRepo.Received().UpdateLastLoginDate(user, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user, ip, \"\", SecurityLogType.Login);\n\t\tvar saltyPassword = password.GetSHA256Hash(salt.Value);\n\t\tawait _mockUserRepo.Received().SetHashedPassword(user, saltyPassword, salt.Value);\n\t}\n\n\t[Fact]\n\tpublic async Task LoginFail()\n\t{\n\t\tconst string email = \"a@b.com\";\n\t\tconst string password = \"fred\";\n\t\tconst string ip = \"1.1.1.1\";\n\t\tvar userService = GetMockedUserService();\n\t\tGuid? salt = null;\n\t\t_mockUserRepo.GetHashedPasswordByEmail(Arg.Any<string>()).Returns(Task.FromResult(Tuple.Create(\"1234\", salt)));\n\n\t\tvar (result, userOut) = await userService.Login(email, password, ip);\n\n\t\tAssert.False(result);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry((User)null, null, ip, \"E-mail attempted: \" + email, SecurityLogType.FailedLogin);\n\t\tawait _mockUserRepo.DidNotReceive().SetHashedPassword(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<Guid>());\n\t}\n\n\t[Fact]\n\tpublic async Task LoginWithUser()\n\t{\n\t\tvar user = UserTest.GetTestUser();\n\t\tvar service = GetMockedUserService();\n\t\tconst string ip = \"1.1.1.1\";\n\n\t\tawait service.Login(user, ip);\n\t\t\t\n\t\tawait _mockUserRepo.Received().UpdateLastLoginDate(user, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user, ip, String.Empty, SecurityLogType.Login);\n\t}\n\n\t[Fact]\n\tpublic async Task LoginWithUserPersistCookie()\n\t{\n\t\tvar user = UserTest.GetTestUser();\n\t\tvar service = GetMockedUserService();\n\t\tconst string ip = \"1.1.1.1\";\n\n\t\tawait service.Login(user, ip);\n\n\t\tawait _mockUserRepo.Received().UpdateLastLoginDate(user, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user, ip, String.Empty, SecurityLogType.Login);\n\t}\n\n\t[Fact]\n\tpublic async Task GetAllRoles()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar list = new List<string>();\n\t\t_mockRoleRepo.GetAllRoles().Returns(Task.FromResult(list));\n\t\tvar result = await userService.GetAllRoles();\n\t\tawait _mockRoleRepo.Received().GetAllRoles();\n\t\tAssert.Same(list, result);\n\t}\n\n\t[Fact]\n\tpublic async Task CreateRole()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tconst string role = \"blah\";\n\t\t\n\t\tawait userService.CreateRole(role, new User(), string.Empty);\n\t\t\n\t\tawait _mockRoleRepo.Received().CreateRole(role);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(Arg.Any<User>(), Arg.Any<User>(), string.Empty, \"Role: blah\", SecurityLogType.RoleCreated);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteRole()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tconst string role = \"blah\";\n\t\tawait userService.DeleteRole(role, default, default);\n\t\tawait _mockRoleRepo.Received().DeleteRole(role);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(Arg.Any<User>(), Arg.Any<User>(), Arg.Any<string>(), \"Role: blah\", SecurityLogType.RoleDeleted);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteRoleThrowsOnAdminOrMod()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(() => userService.DeleteRole(\"Admin\", default, default));\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(() => userService.DeleteRole(\"Moderator\", default, default));\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(() => userService.DeleteRole(\"admin\", default, default));\n\t\tawait Assert.ThrowsAsync<InvalidOperationException>(() => userService.DeleteRole(\"moderator\", default, default));\n\t\tawait _mockRoleRepo.DidNotReceive().DeleteRole(Arg.Any<string>());\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateIsApproved()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar targetUser = GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\tvar user = new User { UserID = 97 };\n\t\tawait userService.UpdateIsApproved(targetUser, true, user, \"123\");\n\t\tawait _mockUserRepo.Received().UpdateIsApproved(targetUser, true);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(user, targetUser, Arg.Any<string>(), String.Empty, SecurityLogType.IsApproved);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateIsApprovedFalse()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar targetUser = GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\tvar user = new User { UserID = 97 };\n\t\tawait userService.UpdateIsApproved(targetUser, false, user, \"123\");\n\t\tawait _mockUserRepo.Received().UpdateIsApproved(targetUser, false);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(user, targetUser, Arg.Any<string>(), String.Empty, SecurityLogType.IsNotApproved);\n\t}\n\n\t[Fact]\n\tpublic async Task UpdateAuthKey()\n\t{\n\t\tvar userService = GetMockedUserService();\n\t\tvar user = GetDummyUser(\"Jeff\", \"a@b.com\");\n\t\tvar key = Guid.NewGuid();\n\t\tawait userService.UpdateAuthorizationKey(user, key);\n\t\tawait _mockUserRepo.Received().UpdateAuthorizationKey(user, key);\n\t}\n\n\t[Fact]\n\tpublic async Task VerifyUserByAuthKey()\n\t{\n\t\tvar key = Guid.NewGuid();\n\t\tconst string name = \"Jeff\";\n\t\tconst string email = \"a@b.com\";\n\t\tvar dummyUser = GetDummyUser(name, email);\n\t\tdummyUser.AuthorizationKey = key;\n\t\tvar userManager = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByAuthorizationKey(key).Returns(Task.FromResult(dummyUser));\n\t\tvar user = await userManager.VerifyAuthorizationCode(dummyUser.AuthorizationKey, \"123\");\n\t\tAssert.Same(dummyUser, user);\n\t\tawait _mockUserRepo.Received().UpdateIsApproved(dummyUser, true);\n\t}\n\n\t[Fact]\n\tpublic async Task VerifyUserByAuthKeyFail()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByAuthorizationKey(Arg.Any<Guid>()).Returns((User)null);\n\t\tvar user = await service.VerifyAuthorizationCode(Guid.NewGuid(), \"123\");\n\t\tAssert.Null(user);\n\t\tawait _mockUserRepo.DidNotReceive().UpdateIsApproved(Arg.Any<User>(), true);\n\t}\n\n\t[Fact]\n\tpublic async Task SearchByEmail()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar list = new List<User>();\n\t\t_mockUserRepo.SearchByEmail(\"blah\").Returns(Task.FromResult(list));\n\t\tvar result = await service.SearchByEmail(\"blah\");\n\t\tAssert.Same(list, result);\n\t\tawait _mockUserRepo.Received().SearchByEmail(\"blah\");\n\t}\n\n\t[Fact]\n\tpublic async Task SearchByName()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar list = new List<User>();\n\t\t_mockUserRepo.SearchByName(\"blah\").Returns(Task.FromResult(list));\n\t\tvar result = await service.SearchByName(\"blah\");\n\t\tAssert.Same(list, result);\n\t\tawait _mockUserRepo.Received().SearchByName(\"blah\");\n\t}\n\n\t[Fact]\n\tpublic async Task SearchByRole()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar list = new List<User>();\n\t\t_mockUserRepo.SearchByRole(\"blah\").Returns(Task.FromResult(list));\n\t\tvar result = await service.SearchByRole(\"blah\");\n\t\tAssert.Same(list, result);\n\t\tawait _mockUserRepo.Received().SearchByRole(\"blah\");\n\t}\n\n\t[Fact]\n\tpublic async Task GetUserEdit()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(new Profile { UserID = 1, Web = \"blah\"}));\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar result = await service.GetUserEdit(user);\n\t\tAssert.Equal(1, result.UserID);\n\t\tAssert.Equal(\"blah\", result.Web);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserProfileOnly()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit();\n\t\tvar profile = new Profile();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\n\t\tawait service.EditUser(user, userEdit, false, false, null, null, \"123\", user);\n\n\t\tawait _mockUserRepo.DidNotReceive().ChangeEmail(Arg.Any<User>(), Arg.Any<string>());\n\t\tawait _mockUserRepo.DidNotReceive().ChangeName(Arg.Any<User>(), Arg.Any<string>());\n\t\tawait _mockUserRepo.DidNotReceive().SetHashedPassword(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<Guid>());\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t}\n\n\tprivate Profile GetReturnedProfile(UserEdit userEdit)\n\t{\n\t\treturn new Profile\n\t\t{\n\t\t\tUserID = 1,\n\t\t\tIsSubscribed = userEdit.IsSubscribed,\n\t\t\tShowDetails = userEdit.ShowDetails,\n\t\t\tIsPlainText = userEdit.IsPlainText,\n\t\t\tHideVanity = userEdit.HideVanity,\n\t\t\tSignature = userEdit.Signature,\n\t\t\tLocation = userEdit.Location,\n\t\t\tDob = userEdit.Dob,\n\t\t\tWeb = userEdit.Web,\n\t\t\tInstagram = userEdit.Instagram,\n\t\t\tFacebook = userEdit.Facebook,\n\t\t};\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserApprovalChange()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1, IsApproved = false};\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit {IsApproved = true};\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(GetReturnedProfile(userEdit)));\n\t\tawait service.EditUser(user, userEdit, false, false, null, null, \"123\", user);\n\t\tawait _mockUserRepo.Received().UpdateIsApproved(user, true);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserNewEmail()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1, Email = \"c@d.com\" };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit { NewEmail = \"a@b.com\" };\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(GetReturnedProfile(userEdit)));\n\t\tawait service.EditUser(user, userEdit, false, false, null, null, \"123\", user);\n\t\tawait _mockUserRepo.Received().ChangeEmail(user, \"a@b.com\");\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserNewPassword()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit { NewPassword = \"foo\" };\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(GetReturnedProfile(userEdit)));\n\t\tawait service.EditUser(user, userEdit, false, false, null, null, \"123\", user);\n\t\tawait _mockUserRepo.Received().SetHashedPassword(user, Arg.Any<string>(), Arg.Any<Guid>());\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserAddRole()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit { Roles = new [] {\"Admin\", \"Moderator\"} };\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(GetReturnedProfile(userEdit)));\n\t\tawait service.EditUser(user, userEdit, false, false, null, null, \"123\", user);\n\t\tawait _mockRoleRepo.Received().ReplaceUserRoles(1, userEdit.Roles);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(Arg.Any<User>(), user, \"123\", \"Admin\", SecurityLogType.UserAddedToRole);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(Arg.Any<User>(), user, \"123\", \"Moderator\", SecurityLogType.UserAddedToRole);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserRemoveRole()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string> { \"Admin\", \"Moderator\" };\n\t\tvar userEdit = new UserEdit { Roles = new[] { \"SomethingElse\" } };\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(GetReturnedProfile(userEdit)));\n\t\tawait service.EditUser(user, userEdit, false, false, null, null, \"123\", user);\n\t\tawait _mockRoleRepo.Received().ReplaceUserRoles(1, userEdit.Roles);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(Arg.Any<User>(), user, \"123\", \"Admin\", SecurityLogType.UserRemovedFromRole);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(Arg.Any<User>(), user, \"123\", \"Moderator\", SecurityLogType.UserRemovedFromRole);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserDeleteAvatar()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User {UserID = 1, Roles = new List<string>()};\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\treturnedProfile.AvatarID = 3;\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tvar profile = new Profile();\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\tawait service.EditUser(user, userEdit, true, false, null, null, \"123\", user);\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t\tAssert.Null(profile.AvatarID);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserNoDeleteAvatar()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\treturnedProfile.AvatarID = 3;\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tvar profile = new Profile();\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\tawait service.EditUser(user, userEdit, false, false, null, null, \"123\", user);\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t\tAssert.Equal(3, profile.AvatarID);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserDeletePhoto()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\treturnedProfile.ImageID = 3;\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tvar profile = new Profile();\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\tawait service.EditUser(user, userEdit, false, true, null, null, \"123\", user);\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t\tAssert.Null(profile.ImageID);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserNoDeletePhoto()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\treturnedProfile.ImageID = 3;\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tvar profile = new Profile();\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\tawait service.EditUser(user, userEdit, false, false, null, null, \"123\", user);\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t\tAssert.Equal(3, profile.ImageID);\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserNewAvatar()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\t_mockProfileRepo.Update(Arg.Any<Profile>()).Returns(Task.FromResult(true));\n\t\t_mockUserAvatarRepo.SaveNewAvatar(1, Arg.Any<byte[]>(), Arg.Any<DateTime>()).Returns(Task.FromResult(12));\n\t\tvar image = new byte[1];\n\n\t\tawait service.EditUser(user, userEdit, false, false, image, null, \"123\", user);\n\n\t\tawait _mockUserAvatarRepo.Received().SaveNewAvatar(1, image, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task EditUserNewPhoto()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tuser.Roles = new List<string>();\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\t_mockProfileRepo.Update(Arg.Any<Profile>()).Returns(Task.FromResult(true));\n\t\t_mockUserImageRepo.SaveNewImage(1, 0, true, Arg.Any<byte[]>(), Arg.Any<DateTime>()).Returns(Task.FromResult(12));\n\t\tvar image = new byte[1];\n\n\t\tawait service.EditUser(user, userEdit, false, false, null, image, \"123\", user);\n\n\t\tawait _mockUserImageRepo.Received().SaveNewImage(1, 0, true, image, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task UserEditPhotosDeleteAvatar()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\treturnedProfile.AvatarID = 3;\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tvar profile = new Profile();\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\tawait service.EditUserProfileImages(user, true, false, null, null);\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t\tawait _mockUserAvatarRepo.Received().DeleteAvatarsByUserID(user.UserID);\n\t\tAssert.Null(profile.AvatarID);\n\t}\n\n\t[Fact]\n\tpublic async Task UserEditPhotosNoDeleteAvatar()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\treturnedProfile.AvatarID = 3;\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tvar profile = new Profile();\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\tawait service.EditUserProfileImages(user, false, false, null, null);\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t\tawait _mockUserAvatarRepo.DidNotReceive().DeleteAvatarsByUserID(user.UserID);\n\t\tAssert.Equal(3, profile.AvatarID);\n\t}\n\n\t[Fact]\n\tpublic async Task UserEditPhotosDeletePhoto()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\treturnedProfile.ImageID = 3;\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tvar profile = new Profile();\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\tawait service.EditUserProfileImages(user, false, true, null, null);\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t\tawait _mockUserImageRepo.Received().DeleteImagesByUserID(user.UserID);\n\t\tAssert.Null(profile.ImageID);\n\t}\n\n\t[Fact]\n\tpublic async Task UserEditPhotosNoDeletePhoto()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar user = new User { UserID = 1 };\n\t\tvar userEdit = new UserEdit();\n\t\tvar returnedProfile = GetReturnedProfile(userEdit);\n\t\treturnedProfile.ImageID = 3;\n\t\t_mockProfileRepo.GetProfile(1).Returns(Task.FromResult(returnedProfile));\n\t\tvar profile = new Profile();\n\t\tawait _mockProfileRepo.Update(Arg.Do<Profile>(x => profile = x));\n\t\tawait service.EditUserProfileImages(user, false, false, null, null);\n\t\tawait _mockProfileRepo.Received().Update(Arg.Any<Profile>());\n\t\tawait _mockUserImageRepo.DidNotReceive().DeleteImagesByUserID(user.UserID);\n\t\tAssert.Equal(3, profile.ImageID);\n\t}\n\n\t[Fact]\n\tpublic async Task GetUsersOnlineCallsRepo()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\tvar users = new List<User>();\n\t\t_mockUserRepo.GetUsersOnline().Returns(Task.FromResult(users));\n\t\tvar result = await service.GetUsersOnline();\n\t\tawait _mockUserRepo.Received().GetUsersOnline();\n\t\tAssert.Same(users, result);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteUserLogs()\n\t{\n\t\tvar targetUser = new User { UserID = 1 };\n\t\tvar user = new User { UserID = 2 };\n\t\tvar service = GetMockedUserService();\n\t\tawait service.DeleteUser(targetUser, user, \"127.0.0.1\", true);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(user, targetUser, \"127.0.0.1\", Arg.Any<string>(), SecurityLogType.UserDeleted);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteUserCallsRepo()\n\t{\n\t\tvar targetUser = new User { UserID = 1 };\n\t\tvar user = new User { UserID = 2 };\n\t\tvar service = GetMockedUserService();\n\t\tawait service.DeleteUser(targetUser, user, \"127.0.0.1\", true);\n\t\tawait _mockUserRepo.Received().DeleteUser(targetUser);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteUserCallsBanRepoIfBanIsTrue()\n\t{\n\t\tvar targetUser = new User { UserID = 1, Email = \"a@b.com\" };\n\t\tvar user = new User { UserID = 2 };\n\t\tvar service = GetMockedUserService();\n\t\tawait service.DeleteUser(targetUser, user, \"127.0.0.1\", true);\n\t\tawait _mockBanRepo.Received().BanEmail(targetUser.Email);\n\t}\n\n\t[Fact]\n\tpublic async Task DeleteUserDoesNotCallBanRepoIfBanIsFalse()\n\t{\n\t\tvar targetUser = new User { UserID = 1, Email = \"a@b.com\" };\n\t\tvar user = new User { UserID = 2 };\n\t\tvar service = GetMockedUserService();\n\t\tawait service.DeleteUser(targetUser, user, \"127.0.0.1\", false);\n\t\tawait _mockBanRepo.DidNotReceive().BanEmail(targetUser.Email);\n\t}\n\n\t[Fact]\n\tpublic async Task ForgotPasswordCallsMailerForGoodUser()\n\t{\n\t\tvar user = new User { UserID = 2, Email = \"a@b.com\" };\n\t\tvar service = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(user.Email).Returns(Task.FromResult(user));\n\t\tawait service.GeneratePasswordResetEmail(user, \"http\");\n\t\tawait _mockForgotMailer.Received(1).ComposeAndQueue(user, Arg.Any<string>());\n\t}\n\n\t[Fact]\n\tpublic async Task ForgotPasswordGeneratesNewAuthKey()\n\t{\n\t\tvar user = new User { UserID = 2, Email = \"a@b.com\" };\n\t\tvar service = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(user.Email).Returns(Task.FromResult(user));\n\t\tawait service.GeneratePasswordResetEmail(user, \"http\");\n\t\tawait _mockUserRepo.Received(1).UpdateAuthorizationKey(user, Arg.Any<Guid>());\n\t}\n\n\t[Fact]\n\tpublic async Task ForgotPasswordThrowsForNoUser()\n\t{\n\t\tvar service = GetMockedUserService();\n\t\t_mockUserRepo.GetUserByEmail(Arg.Any<string>()).Returns((User)null);\n\t\tawait Assert.ThrowsAsync<ArgumentNullException>(async () => await service.GeneratePasswordResetEmail(null, \"http\"));\n\t\tawait _mockForgotMailer.DidNotReceive().ComposeAndQueue(Arg.Any<User>(), Arg.Any<string>());\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/UserSessionServiceTests.cs",
    "content": "namespace PopForums.Test.Services;\n\npublic class UserSessionServiceTests\n{\n\tprivate ISettingsManager _mockSettingsManager;\n\tprivate IUserRepository _mockUserRepo;\n\tprivate IUserSessionRepository _mockUserSessionRepo;\n\tprivate ISecurityLogService _mockSecurityLogService;\n\n\tprivate UserSessionService GetService()\n\t{\n\t\t_mockSettingsManager = Substitute.For<ISettingsManager>();\n\t\t_mockUserRepo = Substitute.For<IUserRepository>();\n\t\t_mockUserSessionRepo = Substitute.For<IUserSessionRepository>();\n\t\t_mockSecurityLogService = Substitute.For<ISecurityLogService>();\n\t\tvar service = new UserSessionService(_mockSettingsManager, _mockUserRepo, _mockUserSessionRepo, _mockSecurityLogService);\n\t\treturn service;\n\t}\n\n\t[Fact]\n\tpublic async Task AnonUserNoCookieGetsCookieAndSessionStart()\n\t{\n\t\tvar service = GetService();\n\t\tvar deleteCalled = false;\n\t\tAction delete = () => { deleteCalled = true; };\n\t\tint? createResult = null;\n\t\tAction<int> create = i => { createResult = i; };\n\n\t\tawait service.ProcessUserRequest(null, null, \"1.1.1.1\", delete, create);\n\t\t\t\n\t\tAssert.False(deleteCalled);\n\t\tAssert.True(createResult.HasValue);\n\t\tawait _mockUserSessionRepo.Received().CreateSession(Arg.Any<int>(), null, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry((int?)null, null, \"1.1.1.1\", createResult.Value.ToString(), SecurityLogType.UserSessionStart);\n\t\tawait _mockUserRepo.DidNotReceive().UpdateLastActivityDate(Arg.Any<User>(), Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task AnonUserWithCookieUpdateSession()\n\t{\n\t\tvar service = GetService();\n\t\tvar deleteCalled = false;\n\t\tAction delete = () => { deleteCalled = true; };\n\t\tint? createResult = null;\n\t\tAction<int> create = i => { createResult = i; };\n\t\tconst int sessionID = 5467;\n\t\t_mockUserSessionRepo.UpdateSession(sessionID, Arg.Any<DateTime>()).Returns(Task.FromResult(true));\n\t\t_mockUserSessionRepo.IsSessionAnonymous(sessionID).Returns(Task.FromResult(true));\n\n\t\tvar result = await service.ProcessUserRequest(null, sessionID, \"1.1.1.1\", delete, create);\n\n\t\tAssert.False(deleteCalled);\n\t\tAssert.Equal(sessionID, result);\n\t\tawait _mockUserSessionRepo.Received().UpdateSession(sessionID, Arg.Any<DateTime>());\n\t\tawait _mockUserRepo.DidNotReceive().UpdateLastActivityDate(Arg.Any<User>(), Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task UserWithAnonCookieStartsLoggedInSession()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tvar deleteCalled = false;\n\t\tAction delete = () => { deleteCalled = true; };\n\t\tint? createResult = null;\n\t\tAction<int> create = i => { createResult = i; };\n\t\tconst int sessionID = 5467;\n\t\t_mockUserSessionRepo.UpdateSession(sessionID, Arg.Any<DateTime>()).Returns(Task.FromResult(true));\n\t\t_mockUserSessionRepo.IsSessionAnonymous(sessionID).Returns(Task.FromResult(true));\n\n\t\tvar result = await service.ProcessUserRequest(user, sessionID, \"1.1.1.1\", delete, create);\n\n\t\tAssert.True(deleteCalled);\n\t\tAssert.Equal(createResult, result);\n\t\tawait _mockUserSessionRepo.Received().UpdateSession(sessionID, Arg.Any<DateTime>());\n\t\tawait _mockUserRepo.Received().UpdateLastActivityDate(user, Arg.Any<DateTime>());\n\t\tawait _mockUserSessionRepo.Received().DeleteSessions(null, sessionID);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, null, String.Empty, sessionID.ToString(), SecurityLogType.UserSessionEnd, Arg.Any<DateTime>());\n\t\tawait _mockUserSessionRepo.Received().CreateSession(Arg.Any<int>(), user.UserID, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user.UserID, Arg.Any<string>(), Arg.Any<string>(), SecurityLogType.UserSessionStart);\n\t}\n\n\t[Fact]\n\tpublic async Task AnonUserWithLoggedInSessionEndsOldOneStartsNewOne()\n\t{\n\t\tvar service = GetService();\n\t\tvar deleteCalled = false;\n\t\tAction delete = () => { deleteCalled = true; };\n\t\tint? createResult = null;\n\t\tAction<int> create = i => { createResult = i; };\n\t\tconst int sessionID = 5467;\n\t\t_mockUserSessionRepo.UpdateSession(sessionID, Arg.Any<DateTime>()).Returns(Task.FromResult(true));\n\t\t_mockUserSessionRepo.IsSessionAnonymous(sessionID).Returns(Task.FromResult(false));\n\n\t\tvar result = await service.ProcessUserRequest(null, sessionID, \"1.1.1.1\", delete, create);\n\n\t\tAssert.True(deleteCalled);\n\t\tAssert.Equal(createResult, result);\n\t\tawait _mockUserSessionRepo.Received().UpdateSession(sessionID, Arg.Any<DateTime>());\n\t\tawait _mockUserSessionRepo.Received().DeleteSessions(null, sessionID);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, null, String.Empty, sessionID.ToString(), SecurityLogType.UserSessionEnd, Arg.Any<DateTime>());\n\t\tawait _mockUserSessionRepo.Received().CreateSession(Arg.Any<int>(), null, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry((int?)null, null, Arg.Any<string>(), Arg.Any<string>(), SecurityLogType.UserSessionStart);\n\t}\n\n\t[Fact]\n\tpublic async Task UserWithNoCookieGetsCookieAndSessionStart()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tvar deleteCalled = false;\n\t\tAction delete = () => { deleteCalled = true; };\n\t\tint? createResult = null;\n\t\tAction<int> create = i => { createResult = i; };\n\t\t_mockUserSessionRepo.GetSessionIDByUserID(Arg.Any<int>()).Returns((ExpiredUserSession)null);\n\n\t\tvar result = await service.ProcessUserRequest(user, null, \"1.1.1.1\", delete, create);\n\n\t\tAssert.False(deleteCalled);\n\t\tAssert.Equal(createResult, result);\n\t\tawait _mockUserSessionRepo.Received().CreateSession(Arg.Any<int>(), 123, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user.UserID, Arg.Any<string>(), result.ToString(), SecurityLogType.UserSessionStart);\n\t\tawait _mockUserRepo.Received().UpdateLastActivityDate(user, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task UserWithCookieUpdateSession()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tvar deleteCalled = false;\n\t\tAction delete = () => { deleteCalled = true; };\n\t\tint? createResult = null;\n\t\tAction<int> create = i => { createResult = i; };\n\t\tconst int sessionID = 5467;\n\t\t_mockUserSessionRepo.UpdateSession(sessionID, Arg.Any<DateTime>()).Returns(Task.FromResult(true));\n\n\t\tvar result = await service.ProcessUserRequest(user, sessionID, \"1.1.1.1\", delete, create);\n\n\t\tAssert.Null(createResult);\n\t\tAssert.False(deleteCalled);\n\t\tAssert.Equal(sessionID, result);\n\t\tawait _mockUserSessionRepo.Received().UpdateSession(sessionID, Arg.Any<DateTime>());\n\t\tawait _mockUserRepo.Received().UpdateLastActivityDate(user, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task UserSessionNoCookieButHasOldSessionEndsOldSessionStartsNewOne()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tAction delete = () => { };\n\t\tint? createResult = null;\n\t\tAction<int> create = i => { createResult = i; };\n\t\tconst int sessionID = 5467;\n\t\t_mockUserSessionRepo.GetSessionIDByUserID(user.UserID).Returns(Task.FromResult(new ExpiredUserSession { UserID = user.UserID, SessionID = sessionID, LastTime = DateTime.MinValue }));\n\n\t\tvar result = await service.ProcessUserRequest(user, sessionID, \"1.1.1.1\", delete, create);\n\t\t\t\n\t\tAssert.Equal(createResult, result);\n\t\tawait _mockUserSessionRepo.Received().DeleteSessions(user.UserID, sessionID);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user.UserID, String.Empty, sessionID.ToString(), SecurityLogType.UserSessionEnd, DateTime.MinValue);\n\t\tawait _mockUserSessionRepo.Received().CreateSession(Arg.Any<int>(), user.UserID, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user.UserID, \"1.1.1.1\", createResult.ToString(), SecurityLogType.UserSessionStart);\n\t\tawait _mockUserRepo.Received().UpdateLastActivityDate(user, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task UserSessionWithNoMatchingIDEndsOldSessionStartsNewOne()\n\t{\n\t\tvar user = new User { UserID = 123 };\n\t\tvar service = GetService();\n\t\tAction delete = () => { };\n\t\tint? createResult = null;\n\t\tAction<int> create = i => { createResult = i; };\n\t\tconst int sessionID = 5467;\n\t\t_mockUserSessionRepo.GetSessionIDByUserID(user.UserID).Returns(Task.FromResult(new ExpiredUserSession { UserID = user.UserID, SessionID = sessionID, LastTime = DateTime.MinValue }));\n\n\t\tvar result = await service.ProcessUserRequest(user, sessionID, \"1.1.1.1\", delete, create);\n\t\t\t\n\t\tAssert.Equal(createResult, result);\n\t\tawait _mockUserSessionRepo.Received().UpdateSession(sessionID, Arg.Any<DateTime>());\n\t\tawait _mockUserSessionRepo.Received().DeleteSessions(user.UserID, sessionID);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user.UserID, String.Empty, sessionID.ToString(), SecurityLogType.UserSessionEnd, DateTime.MinValue);\n\t\tawait _mockUserSessionRepo.Received().CreateSession(Arg.Any<int>(), user.UserID, Arg.Any<DateTime>());\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, user.UserID, Arg.Any<string>(), Arg.Any<string>(), SecurityLogType.UserSessionStart);\n\t\tawait _mockUserRepo.Received().UpdateLastActivityDate(user, Arg.Any<DateTime>());\n\t}\n\n\t[Fact]\n\tpublic async Task CleanExpiredSessions()\n\t{\n\t\tvar service = GetService();\n\t\tvar sessions = new List<ExpiredUserSession>\n\t\t{\n\t\t\tnew ExpiredUserSession { SessionID = 123, UserID = null, LastTime = new DateTime(2000, 2, 5)},\n\t\t\tnew ExpiredUserSession { SessionID = 789, UserID = 456, LastTime = new DateTime(2010, 3, 6)}\n\t\t};\n\t\t_mockUserSessionRepo.GetAndDeleteExpiredSessions(Arg.Any<DateTime>()).Returns(Task.FromResult(sessions));\n\t\t_mockSettingsManager.Current.SessionLength.Returns(20);\n\t\tawait service.CleanUpExpiredSessions();\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, sessions[0].UserID, String.Empty, sessions[0].SessionID.ToString(), SecurityLogType.UserSessionEnd, sessions[0].LastTime);\n\t\tawait _mockSecurityLogService.Received().CreateLogEntry(null, sessions[1].UserID, String.Empty, sessions[1].SessionID.ToString(), SecurityLogType.UserSessionEnd, sessions[1].LastTime);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Test/Services/UserSessionWorkerTests.cs",
    "content": "using NSubstitute.ExceptionExtensions;\n\nnamespace PopForums.Test.Services;\n\npublic class UserSessionWorkerTests\n{\n\tprivate IUserSessionService _userSessionService;\n\tprivate IErrorLog _errorLog;\n\n\tprivate UserSessionWorker GetWorker()\n\t{\n\t\t_userSessionService = Substitute.For<IUserSessionService>();\n\t\t_errorLog = Substitute.For<IErrorLog>();\n\t\treturn new UserSessionWorker(_userSessionService, _errorLog);\n\t}\n\n\t[Fact]\n\tpublic void NoErrorNoLog()\n\t{\n\t\tvar worker = GetWorker();\n\t\t_userSessionService.CleanUpExpiredSessions().Returns(Task.CompletedTask);\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_errorLog.DidNotReceive().Log(Arg.Any<Exception>(), Arg.Any<ErrorSeverity>());\n\t}\n\n\t[Fact]\n\tpublic void LogWhenThrows()\n\t{\n\t\tvar worker = GetWorker(); \n\t\t_userSessionService.CleanUpExpiredSessions().ThrowsAsync<Exception>();\n\t\t\n\t\tworker.Execute();\n\t\t\n\t\t_errorLog.Received().Log(Arg.Any<Exception>(), ErrorSeverity.Error);\n\t}\n}"
  },
  {
    "path": "src/PopForums.Web/Controllers/HomeController.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc;\n\nnamespace PopForums.Mvc.Controllers\n{\n    public class HomeController : Controller\n    {\n\t    public IActionResult Index()\n\t    {\n            return View();\n        }\n\t}\n}\n"
  },
  {
    "path": "src/PopForums.Web/PopForums.Web.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<VersionPrefix>22.0.0</VersionPrefix>\n\t\t<AssemblyName>PopForums.Web</AssemblyName>\n\t\t<PackageId>PopForums.Web</PackageId>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\PopForums.ElasticKit\\PopForums.ElasticKit.csproj\" />\n\t\t<ProjectReference Include=\"..\\PopForums.Mvc\\PopForums.Mvc.csproj\" />\n\t\t<ProjectReference Include=\"..\\PopForums\\PopForums.csproj\" />\n\t\t<ProjectReference Include=\"..\\PopForums.Sql\\PopForums.Sql.csproj\" />\n\t\t<ProjectReference Include=\"..\\PopForums.AzureKit\\PopForums.AzureKit.csproj\" />\n\t\t<PackageReference Include=\"Azure.Extensions.AspNetCore.DataProtection.Blobs\" Version=\"1.5.1\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Folder Include=\"Areas\\Forums\\\" />\n\t\t<Folder Include=\"wwwroot\\\" />\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/PopForums.Web/Program.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.Extensions.DependencyInjection;\nusing PopForums.AzureKit;\nusing PopForums.Mvc.Areas.Forums.Authorization;\nusing PopForums.Mvc.Areas.Forums.Extensions;\nusing PopForums.Sql;\nusing System.Text.Json.Serialization;\nusing PopForums.ElasticKit;\nusing PopForums.Extensions;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.AspNetCore.ResponseCompression;\nusing System.IO.Compression;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.Hosting;\n\nvar builder = WebApplication.CreateBuilder(args);\nvar services = builder.Services;\n\n// the following block is for use when you have multiple nodes running\n// (think Azure App Services) so they can all decode the auth cookie\nvar configuration = builder.Configuration;\nif (configuration[\"DataProtectBlobConnectionString\"] != null)\n{\n\tservices.AddDataProtection()\n\t\t.SetApplicationName(\"popforumsdev\")\n\t\t.PersistKeysToAzureBlobStorage(configuration[\"DataProtectBlobConnectionString\"], \"keys\", \"antiforge\");\n}\n\nservices.AddControllersWithViews();\nservices.AddRazorPages();\n\nservices.Configure<AuthorizationOptions>(options =>\n{\n\t// sets claims policies for admin and moderator functions in POP Forums\n\toptions.AddPopForumsPolicies();\n});\n\nservices.AddMvc(options =>\n{\n\t// identifies users on POP Forums actions\n\toptions.Filters.Add(typeof(PopForumsUserAttribute));\n});\n\nservices.AddControllers().AddJsonOptions(options =>\n{\n\t// Use this to make sure enums are serialized correctly\n\toptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());\n\toptions.JsonSerializerOptions.PropertyNameCaseInsensitive = true;\n});\n\n// set up the dependencies for the SQL library in POP Forums\nservices.AddPopForumsSql();\n// this adds dependencies from the MVC project (and base dependencies) and sets up authentication for the forum\nservices.AddPopForumsMvc();\n\n// use Azure table storage for logging instead of database\n//services.AddPopForumsTableStorageLogging();\n\n// use Redis cache for POP Forums using AzureKit\n//services.AddPopForumsRedisCache();\n\n// required for real-time updating of POP Forums\nservices.AddSignalR();\n// use this instead of previous line if you need to route SignalR messages\n// over a Redis backplane for multi-instance host\n//services.AddSignalR().AddRedisBackplaneForPopForums();\n\n// use Azure Search for POP Forums using AzureKit\n//services.AddPopForumsAzureSearch();\n\n// use ElasticSearch for POP Forums using ElasticKit\n//services.AddPopForumsElasticSearch();\n\n// use Azure Functions queues for POP Forums using AzureKit for background tasks...\n// do NOT call AddPopForumsBackgroundJobs()\nservices.AddPopForumsAzureFunctionsAndQueues();\n\n// persist image uploads to Azure blob storage, see configuration\nservices.AddPopForumsAzureBlobStorageForPostImages();\n\n// creates an instance of the background services for POP Forums... call this last in forum setup,\n// but don't use if you're running these in functions with AddPopForumsAzureFunctionsAndQueues()\n//services.AddPopForumsBackgroundJobs();\n\n// send fewer bits\nservices.AddResponseCompression(options =>\n{\n\toptions.Providers.Add<BrotliCompressionProvider>();\n\toptions.Providers.Add<GzipCompressionProvider>();\n\toptions.EnableForHttps = true;\n});\nservices.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest);\nservices.Configure<BrotliCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest);\n\n\nvar app = builder.Build();\n\nif (!app.Environment.IsDevelopment())\n{\n\t// send fewer bits\n\tapp.UseResponseCompression();\n}\n\n// Records exceptions and info to the POP Forums database.\nvar loggerFactory = app.Services.GetService<ILoggerFactory>();\nloggerFactory.AddPopForumsLogger(app);\n\napp.UseStaticFiles();\n\n// Enables languages\napp.UsePopForumsCultures();\n\n// Not unique to POP Forums, but required. Call before UsePopForumsAuth().\napp.UseAuthentication();\n\napp.UseDeveloperExceptionPage();\n\n// Add MVC to the request pipeline. The order of the next three lines matters:\napp.UseRouting();\n\n// Populate the POP Forums identity in every request.\n// Possible breaking change starting in v21. This must be called after UseRouting()\n// but before UseAuthorization() and endpoint mapping.\napp.UsePopForumsAuth();\n\napp.UseAuthorization();\n\n// POP Forums routes\napp.AddPopForumsEndpoints();\n\n// app routes\napp.MapControllerRoute(\n\t\"areaRoute\",\n\t\"{area:exists}/{controller=Home}/{action=Index}/{id?}\");\napp.MapControllerRoute(\n\t\"default\",\n\t\"{controller=Home}/{action=Index}/{id?}\");\n\napp.Run();"
  },
  {
    "path": "src/PopForums.Web/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"PopForumsKestrel\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"https://localhost:5091;http://localhost:5090\",\n      \"dotnetRunMessages\": true\n    }\n  }\n}"
  },
  {
    "path": "src/PopForums.Web/Views/Home/Index.cshtml",
    "content": "﻿@{\n    ViewData[\"Title\"] = \"Home Page\";\n}\n\n<p><a href=\"/Forums\">forums</a></p>\n\n<p>\n\tPopForums - v@(System.Reflection.Assembly.Load(\"PopForums\").GetName().Version)<br />\n\tPopForums.Sql - v@(System.Reflection.Assembly.Load(\"PopForums.Sql\").GetName().Version)<br />\n\tPopForums.Mvc - v@(System.Reflection.Assembly.Load(\"PopForums.Mvc\").GetName().Version)<br />\n\tPopForums.Web - v@(System.Reflection.Assembly.Load(\"PopForums.Web\").GetName().Version)<br />\n</p>"
  },
  {
    "path": "src/PopForums.Web/Views/Shared/_Layout.cshtml",
    "content": "﻿<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        <title>@ViewBag.Title</title>\n\n        @await RenderSectionAsync(\"HeaderContent\", false)\n    </head>\n    <body>\n        <div class=\"container\">\n            @RenderBody()\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "src/PopForums.Web/Views/_ViewImports.cshtml",
    "content": "﻿@using PopForums.Mvc\n@addTagHelper \"*, Microsoft.AspNetCore.Mvc.TagHelpers\"\n"
  },
  {
    "path": "src/PopForums.Web/Views/_ViewStart.cshtml",
    "content": "﻿@{\n    Layout = \"_Layout\";\n}\n"
  },
  {
    "path": "src/PopForums.Web/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"PopForums\": {\n    \"IpLookupUrlFormat\": \"https://whatismyipaddress.com/ip/{0}\",\n    \"BaseImageBlobUrl\": \"http://127.0.0.1:10000/devstoreaccount1\",\n    \"Storage\": {\n      \"ConnectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"Database\": {\n      \"ConnectionString\": \"server=localhost;Database=popforums21;Trusted_Connection=True;TrustServerCertificate=True;\"\n    },\n    \"Cache\": {\n      \"Seconds\": 180,\n      \"ConnectionString\": \"127.0.0.1:6379,abortConnect=false\",\n      \"ForceLocalOnly\": false\n    },\n    \"Search\": {\n      \"Url\": \"https://localhost:9200\",\n      \"Key\": \"99011A70D3D50D251B0A6141A97B40E7\"\n    },\n    \"Queue\": {\n      \"ConnectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"LogTopicViews\": true,\n    \"ReCaptcha\": {\n      \"UseReCaptcha\": true,\n      \"SiteKey\": \"6Lc2drIUAAAAAPaa1iHozzu0Zt9rjCYHhjk4Jvtr\",\n      \"SecretKey\": \"6Lc2drIUAAAAADXBXpTjMp67L-T5HdLe7OoKlLrG\"\n    },\n    \"RenderBootstrap\": true,\n    \"OAuthOnly\": {\n      \"IsOAuthOnly\": false,\n      \"OAuthClientID\": \"\",\n      \"OAuthClientSecret\": \"\",\n      \"OAuthLoginBaseUrl\": \"\",\n      \"OAuthTokenUrl\": \"\",\n      \"OAuthAdminClaimType\": \"\",\n      \"OAuthAdminClaimValue\": \"\",\n      \"OAuthModeratorClaimType\": \"\",\n      \"OAuthModeratorClaimValue\": \"\",\n      \"OAuthScopes\": \"\",\n      \"OAuthRefreshExpirationMinutes\": \"\"\n    }\n  }\n}"
  }
]