[
  {
    "path": ".coveragerc",
    "content": "[run]\nsource = matrix_registration\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: \"🐛 Bug report\"\ndescription: Report errors or unexpected behavior\nlabels: \n- bug\nbody:\n- type: markdown\n  attributes:\n    value: Please make sure to [search for existing issues](https://github.com/zeratax/matrix-registration/issues) before filing a new one!\n\n- type: dropdown\n  attributes:\n    label: How did you install matrix-registration?\n    multiple: false\n    options:\n      - pip\n      - direct clone from repo\n      - docker\n      - matrix-docker-ansible-deploy\n  validations:\n    required: true\n    \n- type: input\n  attributes:\n    label: What python version are you running?\n    placeholder: \"3.7\"\n    description: Only provide this, when you aren't using docker.\n  validations:\n    required: false\n\n- type: input\n  attributes:\n    label: What version of matrix-registration are you running?\n    placeholder: \"0.9.7\"\n    description: It's fine to write \"latest\" if you updated recently or \"unknown\" if your unsure.\n  validations:\n    required: true\n\n- type: textarea\n  attributes:\n    label: Your config.yml\n    description: This is not always required - if your are unsure, please provide it.\n    placeholder: DO NOT POST PASSWORDS!\n  validations:\n    required: false\n\n- type: textarea\n  attributes:\n    label: Your error log\n    description: Provide it here if you got one.\n    placeholder: |\n        matrix[187]: Traceback (most recent call last):\n        matrix[187]:   File \"/usr/local/lib/python3.8/site-packages/matrix_registration/app.py\", line 9, in <module>\n        matrix[187]:     from flask_limiter.util import get_ipaddr\n        matrix[187]: ImportError: cannot import name 'get_ipaddr' from 'flask_limiter.util'\n        systemd[1]: matrix-registration.service: Main process exited, code=exited, status=1/FAILURE\n    render: text\n  validations:\n    required: false\n      \n- type: dropdown\n  attributes:\n    label: Area of your issue?\n    multiple: false\n    options:\n      - installation\n      - api\n      - general usage\n      - other\n  validations:\n    required: true  \n    \n- type: textarea\n  attributes:\n    label: What happened\n    description: Describe your issue here\n  validations:\n    required: true  \n    \n- type: textarea\n  attributes:\n    label: Steps to reproduce\n    placeholder: Tell us the steps required to trigger your bug.\n  validations:\n    required: false\n    \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: \"\\U0001F4DA Explanation to all config entries\"\n    url: https://github.com/zeratax/matrix-registration/wiki#configuration\n    about: Need help with your config.yaml?\n  - name: \"\\U0001F4DA Examples for api usage\"\n    url: https://github.com/zeratax/matrix-registration/wiki/api\n    about: Need help with using the api?\n  - name: \"\\U0001F310 Our comunity matrix channel\"\n    url: https://matrix.to/#/#matrix-registration:dmnd.sh\n    about: Need general help?"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: \"⭐ Feature / enhancement request\"\ndescription: Suggest an idea for this project\nlabels: \n- enhancement\nbody:\n- type: textarea\n  attributes: \n    label: Description of the new feature / enhancement\n    placeholder: |\n      What is the expected behavior of the proposed feature?\n  validations:\n    required: true\n- type: textarea\n  attributes: \n    label: Scenario when this would be used? \n    placeholder: |\n      What is the scenario this would be used?\n      Is this enhancing to your workflow?\n      Are there benefits for the end-user?\n  validations:\n    required: true\n- type: textarea\n  attributes: \n    label: Further information\n    placeholder: |\n      Do you want to provide anything else? Screenshots? Additional context?\n  validations:\n    required: false\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.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\"\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ master ]\n  schedule:\n    - cron: '24 3 * * 3'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'python' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v1\n      with:\n        languages: ${{ matrix.language }}\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        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v1\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v1\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Publish Docker Image\non: [ push, pull_request ]\n\njobs:\n  build_image:\n    name: Build Docker Image\n    runs-on: ubuntu-latest\n\n    outputs:\n      tag: ${{ steps.vars.outputs.tag }}\n\n    strategy:\n      matrix:\n        arch_name: [ 'x64', 'arm64', 'arm32' ]\n        include:\n        - arch: 'import <nixpkgs> {}'\n          arch_name: 'x64'\n        - arch: 'import <nixpkgs> { crossSystem.config = \"aarch64-unknown-linux-musl\"; }'\n          arch_name: 'arm64'\n        - arch: 'import <nixpkgs> { crossSystem.config = \"armv7l-unknown-linux-musl\"; }'\n          arch_name: 'arm32'\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v2\n      - name: Get the version\n        id: vars\n        run: |\n          tag=\"${GITHUB_REF:10}\"\n          if [[ \"${tag}\" == v* ]]; then\n            echo ::set-output name=tag::$(echo \"${tag}\")\n          else\n            echo ::set-output name=tag::latest\n          fi\n\n      - name: Install nix\n        uses: cachix/install-nix-action@v12\n        with:\n          nix_path: nixpkgs=channel:nixos-22.11\n      - name: Build docker image\n        env:\n          tag: ${{ steps.vars.outputs.tag }}\n        run: nix-build docker.nix --arg pkgs '${{ matrix.arch }}' --argstr tag \"${tag}-${arch_name}\"\n\n      - name: Rename archive \n        run: mv result image_${{ matrix.arch_name }}.tar.gz\n      - uses: actions/upload-artifact@master\n        with:\n          name: image_${{ matrix.arch_name }}\n          path: image_${{ matrix.arch_name }}.tar.gz\n\n  test_image:\n    name: Test Docker Image\n    runs-on: ubuntu-latest\n    needs: build_image\n\n    services:\n      postgres:\n        image: postgres\n        env:\n          POSTGRES_PASSWORD: postgres\n        options:\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v2\n      - uses: actions/download-artifact@master\n        with:\n          name: image_x64\n\n      - name: Run synapse\n        run: |\n          docker run -d \\\n            -e UID=$(id -u) \\\n            -e GID=$(id -g) \\\n            --volume=\"$(pwd)/tests:/data\" \\\n            --network=\"${{ job.services.postgres.network }}\" \\\n            --name=\"synapse\" \\\n            matrixdotorg/synapse:latest\n\n      - name: Load docker image\n        run: docker load < image_x64.tar.gz\n      - name: Run docker image\n        id: token\n        env:\n          tag: ${{ needs.build_image.outputs.tag }}\n        run: |\n          echo \"test alembic\"\n          docker run --rm \\\n            --volume=\"$(pwd)/tests:/data\" \\\n            --network=\"${{ job.services.postgres.network }}\" \\\n            matrix-registration:$tag \\\n            alembic upgrade head\n          echo \"create a token\"\n          docker run --rm \\\n            --volume=\"$(pwd)/tests:/data\" \\\n            --network=\"${{ job.services.postgres.network }}\" \\\n            matrix-registration:$tag \\\n            matrix-registration generate 1> token\n          echo \"serve webpage\"\n          docker run -d \\\n            --volume=\"$(pwd)/tests:/data\" \\\n            --network=\"${{ job.services.postgres.network }}\" \\\n            --publish 5000:5000 \\\n            --name=\"matrix-registration\" \\\n            matrix-registration:$tag\n\n          echo ::set-output name=token::$(cat token)\n\n      - name: Register test account\n        run: |\n          echo \"waiting until matrix-registration is up...\"\n          for run in {1..5}; do\n            healthy=$(docker inspect -f \"{{.State.Health.Status}}\" matrix-registration)\n            echo $healthy\n            if [ \"$healthy\" = \"healthy\" ]; then\n              echo \"matrix-registration is up!\"\n              break\n            else\n              sleep 1\n            fi\n          done\n          echo \"waiting until synapse is up...\"\n          for run in {1..60}; do\n            healthy=$(docker inspect -f \"{{.State.Health.Status}}\" synapse)\n            echo $healthy\n            if [ \"$healthy\" = \"healthy\" ]; then\n              echo \"synapse is up!\"\n              break\n            else\n              sleep 1\n            fi\n          done\n          # register account\n          curl -fSs \\\n            -F 'username=test' \\\n            -F 'password=verysecure' \\\n            -F 'confirm=verysecure' \\\n            -F 'token=${{ steps.token.outputs.token }}' \\\n            http://matrix-registration:5000/register\n \n      - name: Registering failed, check logs\n        if: ${{ failure() }}\n        run: |\n          cat tests/mreg.log\n          docker logs synapse\n\n      - name: Stop manually started containers\n        if: ${{ always() }}\n        run: docker kill $(docker ps -q)\n\n  push_to_registries:\n    name: Push Docker Image to Multiple Registries\n    runs-on: ubuntu-latest\n    needs: test_image\n    if: startsWith(github.ref, 'refs/tags/v')\n\n    strategy:\n      matrix:\n        hub: [Github, Docker]\n        include:\n          - hub: Github\n            registry: ghcr.io\n            username: DOCKER_USERNAME\n            password: GITHUB_TOKEN\n            image: \"/matrix-registration-image\"\n          - hub: Docker\n            registry: docker.io\n            username: DOCKER_USERNAME\n            password: DOCKER_PASSWORD\n            image: \"\"\n\n    steps:\n      - uses: actions/download-artifact@master\n        with:\n          name: image\n\n      # ${GITHUB_REPOSITORY,,} => zeratax/matrix-registration\n      - name: Push to ${{ matrix.hub }} \n        run: |\n          echo \"login to registry...\"\n          docker login --username=\"${{secrets[matrix.username]}}\" --password=\"${{secrets[matrix.password]}}\" ${{ matrix.registry }}\n\n          echo \"logged in!\"\n\n          arches=(x64 amd64 amd32)\n          image_names=()\n\n          echo \"uploading images for individual arches...\"\n          for arch in $arches\n          do\n            image=\"image_${arch}.tar.gz\"\n\n            echo \"check if image '${image}' for arch ${arch} exists...\"\n            if [ ! -f \"${image}\" ]; then\n              break\n            fi\n\n            echo \"image exists!\"\n\n            full_tag=\"${tag}-${arch}\"\n            name=\"${GITHUB_REPOSITORY,,}${{matrix.image}}\"\n\n            echo \"upload image for arch ${arch}...\"\n            docker load -i $image\n            docker push ${name}:${full_tag}\n\n            image_names=+(\"${name}:${full_tag}\")\n            echo \"so far uploaded images: '${#image_names[@]}'\"\n          done\n\n          if [ ${#image_names[@]} -eq 0 ]; then\n            echo \"no images uploaded!\"\n            return 1\n          fi\n\n          echo \"creating a manifest to associate arch images with multi-arch image...\"\n          docker manifest create --amend ${name}:${tag} ${image_names[@]}\n          echo \"uploading manifest...\"\n          docker manifest push ${name}:${tag}\n          echo \"success!\"\n        env:\n          tag: ${{ needs.build_image.outputs.tag }}\n"
  },
  {
    "path": ".github/workflows/pypi.yml",
    "content": "name: Publish Python Package\n\non:\n  push:\n    # Sequence of patterns matched against refs/tags\n    tags:\n      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\njobs:\n  build-and-publish:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@master\n    - name: Set up Python 3.9\n      uses: actions/setup-python@v1\n      with:\n        python-version: 3.9\n    - name: Install pypa/build\n      run: >-\n        python -m\n        pip install\n        build\n        --user\n    - name: Build a binary wheel 👷\n      run: >-\n        python -m\n        build\n        --wheel\n        --outdir dist/\n        .\n    - name: Publish Python 🐍 distributions 📦 to PyPI\n      uses: pypa/gh-action-pypi-publish@master\n      with:\n        password: ${{ secrets.PYPI_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n    branches:\n    - master\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        python-version: [ '3.7', '3.8', '3.9' ]\n    name: Python ${{ matrix.python-version }}\n    steps:\n      - uses: actions/checkout@v2\n      - name: Setup python\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          python setup.py develop\n      - name: Lint with flake8 👕\n        run: |\n          pip install flake8\n          # stop the build if there are Python syntax errors or undefined names\n          flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics\n          # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide\n          flake8 matrix_registration --per-file-ignores=\"__init__.py:F401\" --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics\n      - name: Test with parameterized unit tests 🚨\n        run: |\n          python setup.py test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Project specific\n*.yaml\n*.conf\n!config.sample.yaml\n!tests/test_config.yaml\nresult\ndata/\n\n!matrix_registration/translations/*.yaml\n# vscode\n.vscode/\n# nose\nman/\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\nshare/\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n#  Virtualenv\n# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/\n.Python\n[Bb]in\n[Ii]nclude\n[Ll]ib\n[Ll]ib64\n[Ll]ocal\n[Ss]cripts\npyvenv.cfg\n.venv\npip-selfcheck.json\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: python\nmatrix:\n  include:\n    - python: 3.7\n      dist: focal\n      sudo: true\n    - python: 3.8\n      dist: focal\n      sudo: true\n    - python: 3.9\n      dist: focal\n      sudo: true\ninstall: \n  - pip install tox-travis\n  - pip install coveralls\nscript: tox\n\nafter_success:\n    coveralls\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\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\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\n 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\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at mail@zera.tax. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# CONTRIBUTING\n## Code of Conduct\nSee [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)\n## How Can I Contribute?\n### Issues\nFilling issues is a great and easy way to help find bugs and get new features implemented.\n#### Bugs\nIf you're reporting a security issue, please email me at security@zera.tax, otherwise\nif you're reporting a bug, please fill out this [form](https://github.com/ZerataX/matrix-registration/issues/new?labels=bug&template=bug_report.md).\n#### Feature Requests\nIf you have an idea for a new feature or an enhancement, please fill out this [form](https://github.com/ZerataX/matrix-registration/issues/new?labels=enhancement&template=feature_request.md).\n#### Translations\nYou can translate the registration page over at https://l10n.dmnd.sh/engage/matrix-registration/\n\n[![Translation status](https://l10n.dmnd.sh/widgets/matrix-registration/-/open-graph.png)](https://l10n.dmnd.sh/engage/matrix-registration/)\n\n#### Getting Started\n\nTo begin working on translating with WebLate, you will need to create an account linked to your GitHub account. From there, you'll be able to see a list of currently translated languages as well as incomplete lines.\n\n\nIf you have any further questions about how to contribute, please make an issue on the GitHub page.\n\n\n### Pull Requests\n\nEvery PR should not break tests and ideally include tests for added functionality.\nAlso it is recommend to follow the [PEP8](https://www.python.org/dev/peps/pep-0008/) styleguide\n#### Setting up the Project\n\n```bash\ngit clone https://github.com/ZerataX/matrix-registration.git\ncd matrix-registration\n\nvirtualenv -p /usr/bin/python3.6 .\nsource ./bin/activate\n\npython setup.py develop\ncp config.sample.yaml config.yaml\n```\n\nand edit config.yaml\n\nYou can run tests by executing the following command in the project root\n```bash\npython setup.py test\n```\n\n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Jona Abdinghoff\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"resources/logo.png\" width=\"300\">\n\n[![Build Status](https://travis-ci.org/ZerataX/matrix-registration.svg?branch=master)](https://travis-ci.org/ZerataX/matrix-registration) [![Coverage Status](https://coveralls.io/repos/github/ZerataX/matrix-registration/badge.svg)](https://coveralls.io/github/ZerataX/matrix-registration) [![Translation status](https://l10n.dmnd.sh/widgets/matrix-registration/-/svg-badge.svg)](http://l10n.dmnd.sh/engage/matrix-registration/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/matrix-registration.svg) [![PyPI](https://img.shields.io/pypi/v/matrix-registration.svg)](https://pypi.org/project/matrix-registration/) [![Docker Pulls](https://img.shields.io/docker/pulls/zeratax/matrix-registration)](https://hub.docker.com/r/zeratax/matrix-registration) [![Matrix](https://img.shields.io/matrix/matrix-registration:dmnd.sh.svg?server_fqdn=matrix.org)](https://matrix.to/#/#matrix-registration:dmnd.sh)\n\n# matrix-registration\n\nA simple Python application enabling token-based registration for matrix servers.\n\nYou may have, like me, encountered the situation where you want to invite your friends to create an account on your homeserver, but neither want to open up public registration nor create accounts for every individual user yourself. This project aims to solve this problem.\n\nWith matrix-registration, you can quickly generate tokens on the fly and share them with your friends to allow them to register on your homeserver.\n\n<img src=\"https://matrix.org/_matrix/media/v1/download/dmnd.sh/UKGgpbHRdFXzKywxjjbfHAsI\" width=\"500\">\n\n\n## Setup\nInstall using pip:\n\n```bash\npip3 install matrix-registration\n```\n\nor check the [docker guide](https://github.com/ZerataX/matrix-registration/wiki/docker)\n\n### First start\nTo start, execute `matrix-registration`.\n\nA configuration file should be generated for you on first start.\n\n**Note:**\nFor `server_location` it is recommended to use a local connect, e.g. `localhost:8008` (or whatever port synapse listens to).\nIt is possible however to connect over the internet, but you will need to make sure `/_synapse/admin/v1/register` is accessible.\n\n<details>\n  <summary> If the configuration file is not automatically discovered...</summary>\n  \nyou can create a configuration by copying [config.sample.yaml](/config.sample.yaml) to your server and editing it:\n```bash\nwget https://raw.githubusercontent.com/ZerataX/matrix-registration/master/config.sample.yaml\ncp config.sample.yaml config.yaml\nnano config.yaml\n```\n\nThen pass the path to this configuration to the application on startup using `--config-path /path/to/config.yaml`.\n</details>\n\n__INFO:__ \n- This only asks you for the most important options. \nYou should definitely take a look at the actual configuration file. The path to the file will be printed by `matrix-registration` the first time it runs.\n\n## Usage\n\n```bash\n$ matrix-registration -h\nUsage: matrix-registration [OPTIONS] COMMAND [ARGS]...\n\n  a token based matrix registration app\n\nOptions:\n  --config-path TEXT  specifies the config file to be used\n  --version           Show the flask version\n  -h, --help          Show this message and exit.\n\nCommands:\n  generate  generate new token\n  serve     start api server\n  status    view status or disable\n\n```\n\nAfter you've started the API server and [generated a token](https://github.com/ZerataX/matrix-registration/wiki/api#creating-a-new-token) you can register an account either:\n- with a simple post request, e.g.:\n```bash\ncurl -X POST \\\n     -F 'username=test' \\\n     -F 'password=verysecure' \\\n     -F 'confirm=verysecure' \\\n     -F 'token=DoubleWizardSki' \\\n     http://localhost:5000/register\n```\n- or by visiting http://localhost:5000/register?token=DoubleWizardSki\n\n\n## Further Resources\n\n### Nginx reverse-proxy\n\nIf you'd like to run matrix-registration behind a reverse-proxy, here is an example nginx setup:\n\n```nginx\nlocation  ~ ^/(static|register) {\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_pass http://localhost:5000;\n}\n```\n\nIf you'll be using the [web API](https://github.com/ZerataX/matrix-registration/wiki/api), you'll also need to forward that endpoint. More information on reverse proxying [here](https://github.com/ZerataX/matrix-registration/wiki/reverse-proxy#optional)\n\n### Custom registration page\n\nIf you want to write your own registration page, you can take a look at the sample in [resources/example.html](resources/example.html)\n\nThe html page looks for the query paramater `token` and sets the token input field to it's value. this would allow you to directly share links with the token included, e.g.:\n\n`https://homeserver.tld/register.html?token=DoubleWizardSki`\n\nIf you already have a website and want to use your own register page, the [wiki](https://github.com/ZerataX/matrix-registration/wiki/reverse-proxy#advanced) describes a more advanced nginx setup.\n\n\n### bot\n\nif you're looking for a bot to interface with matrix-registration and manage your tokens, take a look at:\n\n[maubot-invite](https://github.com/williamkray/maubot-invite)\n\n\n### Similar projects\n\n  - [matrix-invite](https://gitlab.com/reivilibre/matrix-invite) live at https://librepush.net/matrix/registration/\n  - [matrix-register-bot](https://github.com/krombel/matrix-register-bot) using a bot to review accounts before sending out invite links\n  - [MatrixRegistration](https://gitlab.com/olze/matrixregistration/) similar java project using my webui\n  - [Mother Miounne](https://gitlab.com/etke.cc/miounne) \"A bridge between matrix and external services\", which also integrates matrix-registration\n\nFor more info check the [wiki](https://github.com/ZerataX/matrix-registration/wiki)\n\n### Artwork attribution\n\n- The valley cover photo on the registration page is photo by [Jesús Roncero](https://www.flickr.com/golan)\nused under the terms of [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). No warranties are given.\n- The font used on the registration page is [Nunito](https://fonts.google.com/specimen/Nunito) which is licensed under [SIL Open Font License, Version 1.1](./matrix_registration/static/fonts/NUNITO-LICENSE).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\nThis project is still in active develoment so be aware that only the latest version will receive security fixes.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| latest  | :white_check_mark: |\n| < 1.0   | :x:                |\n\n\n## Reporting a Vulnerability\nIf you happen to find a vulnerability, please send an E-mail using [this template](mailto:mail@zera.tax?Subject=matrix-registration%20vulnerability%20%7Bshort%20description%7D) including theese informations:\n\n- What kind of issue is it (leaked data, skipped authentification, ...)\n- How critical is the issue\n- Steps to reproduce\n- If possible: How can we contact you (E-Mail, matrix account)\n\nIf it's a critical issue we will and try to fix it ASAP (note that this is a hobby project by a very limited number of people)\n"
  },
  {
    "path": "alembic/README",
    "content": ""
  },
  {
    "path": "alembic/env.py",
    "content": "from logging.config import fileConfig\n\nfrom sqlalchemy import engine_from_config\nfrom sqlalchemy import pool\n\nfrom alembic import context\n\nimport sys\nfrom os import getcwd\nfrom os.path import abspath, dirname\n\nsys.path.insert(0, dirname(dirname(abspath(__file__))))\n\nfrom matrix_registration import config as mr_config\n\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\n\n# load matrix-registration config and set db path for alembic\nconfig_path = context.get_x_argument(as_dictionary=True).get(\"config\") or \"config.yaml\"\nmr_config.config = mr_config.Config(config_path)\nconfig.set_main_option(\"sqlalchemy.url\", mr_config.config.db.replace(\"{cwd}\", f\"{getcwd()}\"))\nprint(config.get_main_option(\"sqlalchemy.url\"))\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# target_metadata = mymodel.Base.metadata\ntarget_metadata = None\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True,\n        dialect_opts={\"paramstyle\": \"named\"},\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n    connectable = engine_from_config(\n        config.get_section(config.config_ini_section),\n        prefix=\"sqlalchemy.\",\n        poolclass=pool.NullPool,\n    )\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            render_as_batch=config.get_main_option('sqlalchemy.url').startswith('sqlite:///')\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "alembic/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "alembic/versions/130b5c2275d8_update_ip_token_association.py",
    "content": "'''update ip token association\n\nRevision ID: 130b5c2275d8\nRevises: 140a25d5f185\nCreate Date: 2021-07-10 20:40:46.937634\n\n'''\nfrom alembic import op\nimport sqlalchemy as sa\nfrom sqlalchemy import Table, Column, Integer, String, ForeignKey\nfrom sqlalchemy.engine.reflection import Inspector\nfrom flask_sqlalchemy import SQLAlchemy\n\n# revision identifiers, used by Alembic.\nrevision = '130b5c2275d8'\ndown_revision = '140a25d5f185'\nbranch_labels = None\ndepends_on = None\n\ndb = SQLAlchemy()\nconn = op.get_bind()\n\ndef upgrade():\n        ips = conn.execute('select id, address from ips').fetchall()\n        associations = conn.execute('select ips, tokens from association').fetchall()\n\n        final_associations = []\n        for association in associations:\n            association_ip, association_token = association\n            for ip in ips:\n                id, ip_address = ip\n                if ip_address == association_ip:\n                    final_associations.append({'ips': id, 'tokens': association_token})\n\n        op.drop_table('association')\n\n        association = op.create_table(\n            'association', db.Model.metadata,\n                Column('ips', Integer, ForeignKey('ips.id'), primary_key=True),\n                Column('tokens', String(255), ForeignKey('tokens.name'), primary_key=True)\n        )   \n\n\n        op.bulk_insert(association, final_associations)\n\n        \n\ndef downgrade():\n    ips = conn.execute('select id, address from ips').fetchall()\n    associations = conn.execute('select ips, tokens from association').fetchall()\n\n    final_associations = []\n    for association in associations:\n        association_ip, association_token = association\n        for ip in ips:\n            id, ip_address = ip\n            if id == association_ip:\n                final_associations.append({'ips': ip_address, 'tokens': association_token})\n\n    op.drop_table('association')\n\n    association = op.create_table(\n        'association', db.Model.metadata,\n            Column('ips', String(255), ForeignKey('ips.address'), primary_key=True),\n            Column('tokens', String(255), ForeignKey('tokens.name'), primary_key=True)\n    )   \n\n\n    op.bulk_insert(association, final_associations)\n"
  },
  {
    "path": "alembic/versions/140a25d5f185_create_tokens_table.py",
    "content": "\"\"\"create tokens table\n\nRevision ID: 140a25d5f185\nRevises: \nCreate Date: 2020-12-12 01:44:28.195736\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\nfrom sqlalchemy import Table, Column, Integer, String, Boolean, DateTime, ForeignKey\nfrom sqlalchemy.engine.reflection import Inspector\nfrom flask_sqlalchemy import SQLAlchemy\n\n\n# revision identifiers, used by Alembic.\nrevision = '140a25d5f185'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\ndb = SQLAlchemy()\n\n\ndef upgrade():\n    conn = op.get_bind()\n    inspector = Inspector.from_engine(conn)\n    tables = inspector.get_table_names()\n\n    if 'ips' not in tables:\n        op.create_table(\n            'ips',\n            sa.Column('id', sa.Integer, primary_key=True),\n            sa.Column('address', sa.String(255), nullable=True)\n        )\n\n    if 'tokens' not in tables:\n        op.create_table(\n            'tokens',\n            sa.Column('name', String(255), primary_key=True),\n            sa.Column('expiration_date', DateTime, nullable=True),\n            sa.Column('max_usage', Integer, default=1),\n            sa.Column('used', Integer, default=0),\n            sa.Column('disabled', Boolean, default=False),\n            sa.Column('ips', Integer, ForeignKey('association.id'))\n        )\n    else:\n        try:\n            with op.batch_alter_table('tokens') as batch_op:\n                batch_op.alter_column('ex_date', new_column_name='expiration_date', nullable=True)\n                batch_op.alter_column('one_time', new_column_name='max_usage')\n\n                batch_op.add_column(\n                    Column('disabled', Boolean, default=False)\n                )\n        except KeyError:\n            pass\n\n\n    if 'association' not in tables:\n        op.create_table(\n        'association', db.Model.metadata,\n            Column('ips', String, ForeignKey('ips.address'), primary_key=True),\n            Column('tokens', Integer, ForeignKey('tokens.name'), primary_key=True)\n        )   \n    \n    op.execute(\"update tokens set expiration_date=null where expiration_date='None'\")\n\n\n\ndef downgrade():\n    op.alter_column('tokens', 'expiration_date', new_column_name='ex_date')\n    op.alter_column('tokens', 'max_usage', new_column_name='one_time')\n"
  },
  {
    "path": "alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# path to migration scripts\nscript_location = alembic\n\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# timezone to use when rendering the date\n# within the migration file as well as the filename.\n# string value is passed to dateutil.tz.gettz()\n# leave blank for localtime\n# timezone =\n\n# max length of characters to apply to the\n# \"slug\" field\n#truncate_slug_length = 40\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n# set to 'true' to allow .pyc and .pyo files without\n# a source .py file to be detected as revisions in the\n# versions/ directory\n# sourceless = false\n\n# version location specification; this defaults\n# to alembic/versions.  When using multiple version\n# directories, initial revisions must be specified with --version-path\n# version_locations = %(here)s/bar %(here)s/bat alembic/versions\n\n# the output encoding used when revision files\n# are written from script.py.mako\n# output_encoding = utf-8\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "config.sample.yaml",
    "content": "server_location: 'http://localhost:8008'\nserver_name: 'matrix.org'\nregistration_shared_secret: 'RegistrationSharedSecret' # see your synapse's homeserver.yaml\nadmin_api_shared_secret: 'APIAdminPassword' # to generate tokens via the web api\nbase_url: '' # e.g. '/element' for https://example.tld/element/register\nclient_redirect: 'https://app.element.io/#/login'\nclient_logo: 'static/images/element-logo.png' # use '{cwd}' for current working directory\ndb: 'sqlite:///{cwd}/db.sqlite3'\nhost: 'localhost'\nport: 5000\nrate_limit: [\"100 per day\", \"10 per minute\"]\nallow_cors: false\nip_logging: false\nlogging:\n  disable_existing_loggers: false\n  version: 1\n  root:\n    level: DEBUG\n    handlers: [console, file]\n  formatters:\n    brief:\n      format: '%(name)s - %(levelname)s - %(message)s'\n    precise:\n      format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'\n  handlers:\n    console:\n      class: logging.StreamHandler\n      level: INFO\n      formatter: brief\n      stream: ext://sys.stdout\n    file:\n      class: logging.handlers.RotatingFileHandler\n      formatter: precise\n      level: INFO\n      filename: m_reg.log\n      maxBytes: 10485760 # 10MB\n      backupCount: 3\n      encoding: utf8\n# password requirements\npassword:\n  min_length: 8\n# username requirements\nusername:\n  validation_regex: [] #list of regexes that the selected username must match.        Example: '[a-zA-Z]\\.[a-zA-Z]'\n  invalidation_regex: [] #list of regexes that the selected username must NOT match.  Example: '(admin|support)'\n"
  },
  {
    "path": "default.nix",
    "content": "{ pkgs ? import <nixpkgs> { } }:\nwith pkgs.python3.pkgs;\n\nbuildPythonPackage rec {\n  name = \"matrix-registration\";\n  src = builtins.path {\n    inherit name;\n    path = ./.;\n  };\n\n  postPatch = ''\n    sed -i -e '/alembic>/d' setup.py\n  '';\n\n  propagatedBuildInputs = [\n    appdirs\n    flask\n    flask-cors\n    flask-httpauth\n    flask-limiter\n    flask_sqlalchemy\n    jsonschema\n    python-dateutil\n    pyyaml\n    requests\n    waitress\n    wtforms\n    psycopg2\n  ];\n\n  checkInputs = [ parameterized ];\n}\n"
  },
  {
    "path": "docker.nix",
    "content": "{ pkgs ? import <nixpkgs> { }, tag ? \"latest\" }:\n\nlet\n  matrix-registration-config = \"/data/config.yaml\";\n\n  python3 = let\n    packageOverrides = self: super: rec {\n      alembic = super.alembic.overridePythonAttrs (old: {\n        makeWrapperArgs = [\n          \"--chdir '${matrix-registration}'\"\n          ''--add-flags \"-x config='${matrix-registration-config}'\"''\n        ];\n      });\n      matrix-registration =\n        (import ./default.nix { inherit pkgs; }).overridePythonAttrs (old: {\n          makeWrapperArgs =\n            [ ''--add-flags \"--config-path='${matrix-registration-config}'\"'' ];\n        });\n    };\n  in pkgs.python3.override {\n    inherit packageOverrides;\n    # enableOptimizations = true;\n    # reproducibleBuild = false;\n    self = python3;\n  };\n\n  python-packages = ps: with ps; [ matrix-registration alembic ];\n\nin pkgs.dockerTools.buildImage {\n  name = \"matrix-registration\";\n  tag = tag;\n  created = \"now\";\n\n  copyToRoot = python3.withPackages python-packages;\n\n  config = {\n    CMD = [ \"matrix-registration\" \"serve\"];\n    WorkingDir = \"/data\";\n    Volumes = { \"/data\" = { }; };\n    ExposedPorts = { \"5000/tcp\" = { }; };\n    HealthCheck = {\n      Interval = 3000000000;\n      Timeout = 1000000000;\n      StartPeriod = 3000000000;\n      Test =\n        [ \"CMD\" \"${pkgs.curl}/bin/curl\" \"-fSs\" \"http://localhost:5000/health\" ];\n    };\n  };\n}\n"
  },
  {
    "path": "matrix_registration/__init__.py",
    "content": "from . import api\nfrom . import tokens\nfrom . import config\n\n__version__ = \"0.9.2.dev3\"\nname = \"matrix_registration\"\n"
  },
  {
    "path": "matrix_registration/api.py",
    "content": "# Standard library imports...\nimport logging\nimport os\nimport re\nfrom datetime import datetime\n\n# Third-party imports...\nfrom flask import (\n    Blueprint,\n    abort,\n    jsonify,\n    request,\n    make_response,\n    render_template,\n    send_file,\n)\nfrom flask_httpauth import HTTPTokenAuth\nfrom requests import exceptions\nfrom werkzeug.exceptions import BadRequest\nfrom wtforms import Form, StringField, PasswordField, validators\n\n# Local imports...\nfrom . import config\nfrom . import tokens\nfrom .constants import __location__\nfrom .limiter import limiter, get_default_rate_limit\nfrom .matrix_api import create_account\nfrom .translation import get_translations\n\nauth = HTTPTokenAuth(scheme=\"SharedSecret\")\nlogger = logging.getLogger(__name__)\n\napi = Blueprint(\"api\", __name__)\nhealthcheck = Blueprint(\"healthcheck\", __name__)\nlimiter.limit(get_default_rate_limit)(api)\n\n\ndef validate_token(form, token):\n    \"\"\"\n    validates token\n\n    Parameters\n    ----------\n    arg1 : Form object\n    arg2 : str\n        token name, e.g. 'DoubleWizardSki'\n\n    Raises\n    -------\n    ValidationError\n        Token is invalid\n\n    \"\"\"\n    tokens.tokens.load()\n    if not tokens.tokens.active(token.data):\n        raise validators.ValidationError(\"Token is invalid\")\n\n\ndef validate_username(form, username):\n    \"\"\"\n    validates username\n\n    Parameters\n    ----------\n    arg1 : Form object\n    arg2 : str\n        username name, e.g: '@user:matrix.org' or 'user'\n        https://github.com/matrix-org/matrix-doc/blob/master/specification/appendices/identifier_grammar.rst#user-identifiers\n    Raises\n    -------\n    ValidationError\n        Username doesn't follow mxid requirements\n    \"\"\"\n    re_mxid = f\"^(?P<at>@)?(?P<username>[a-zA-Z_\\-=\\.\\/0-9]+)(?P<server_name>:{re.escape(config.config.server_name)})?$\"\n    match = re.search(re_mxid, username.data)\n    if not match:\n        raise validators.ValidationError(\n            f\"Username doesn't follow mxid pattern: /{re_mxid}/\"\n        )\n    username = match.group(\"username\")\n    for e in [\n        validators.ValidationError(f\"Username does not follow custom pattern /{x}/\")\n        for x in config.config.username[\"validation_regex\"]\n        if not re.search(x, username)\n    ]:\n        raise e\n    for e in [\n        validators.ValidationError(f\"Username must not follow custom pattern /{x}/\")\n        for x in config.config.username[\"invalidation_regex\"]\n        if re.search(x, username)\n    ]:\n        raise e\n\n\ndef validate_password(form, password):\n    \"\"\"\n    validates username\n\n    Parameters\n    ----------\n    arg1 : Form object\n    arg2 : str\n        password\n    Raises\n    -------\n    ValidationError\n        Password doesn't follow length requirements\n    \"\"\"\n    min_length = config.config.password[\"min_length\"]\n    err = \"Password should be between %s and 255 chars long\" % min_length\n    if len(password.data) < min_length or len(password.data) > 255:\n        raise validators.ValidationError(err)\n\n\nclass RegistrationForm(Form):\n    \"\"\"\n    Registration Form\n\n    validates user account registration requests\n    \"\"\"\n\n    username = StringField(\n        \"Username\",\n        [\n            validators.Length(min=1, max=200),\n            # validators.Regexp(re_mxid)\n            validate_username,\n        ],\n    )\n    password = PasswordField(\n        \"New Password\",\n        [\n            # validators.Length(min=8),\n            validate_password,\n            validators.DataRequired(),\n            validators.EqualTo(\"confirm\", message=\"Passwords must match\"),\n        ],\n    )\n    confirm = PasswordField(\"Repeat Password\")\n    token = StringField(\n        \"Token\", [validators.Regexp(r\"^([A-Z][a-z]+)+$\"), validate_token]\n    )\n\n\ndef get_request_ips(request):\n    \"\"\"\n    Get the chain of client and proxy IP addresses from the request as\n    a nonempty list, where the closest IP in the chain is last. Each\n    IP vouches only for the IP before it. This works best if all proxies\n    conform the to the X-Forwarded-For header spec, including whatever\n    reverse proxy (such as nginx) is directly in front of the app, if any.\n    (X-Real-IP and similar are not supported at this time.)\n    \"\"\"\n    return request.headers.getlist(\"X-Forwarded-For\") + [request.remote_addr]\n\n\n@auth.verify_token\ndef verify_token(token):\n    return (\n        token != \"APIAdminPassword\" and token == config.config.admin_api_shared_secret\n    )\n\n\n@auth.error_handler\ndef unauthorized():\n    resp = {\"errcode\": \"MR_BAD_SECRET\", \"error\": \"wrong shared secret\"}\n    return make_response(jsonify(resp), 401)\n\n\n@api.route(\"/static/replace/images/element-logo.png\")\ndef element_logo():\n    return send_file(\n        config.config.client_logo.replace(\"{cwd}\", f\"{os.getcwd()}/\"),\n        mimetype=\"image/jpeg\",\n    )\n\n\n@api.route(\"/register\", methods=[\"GET\", \"POST\"])\ndef register():\n    \"\"\"\n    main user account registration endpoint\n    to register an account you need to send a\n    application/x-www-form-urlencoded request with\n      - username\n      - password\n      - confirm\n      - token\n    as described in the RegistrationForm\n    \"\"\"\n    if request.method == \"POST\":\n        logger.debug(\"an account registration started...\")\n        form = RegistrationForm(request.form)\n        logger.debug(\"validating request data...\")\n        if form.validate():\n            logger.debug(\"request valid\")\n            return create_account_from_form(form)\n\n        logger.debug(\"account creation failed!\")\n        resp = {\"errcode\": \"MR_BAD_USER_REQUEST\", \"error\": form.errors}\n        return make_response(jsonify(resp), 400)\n\n    # GET REQUEST\n    server_name = config.config.server_name\n    pw_length = config.config.password[\"min_length\"]\n    uname_regex = config.config.username[\"validation_regex\"]\n    uname_regex_inv = config.config.username[\"invalidation_regex\"]\n    lang = request.args.get(\"lang\") or request.accept_languages.best\n    replacements = {\"server_name\": server_name, \"pw_length\": pw_length}\n    translations = get_translations(lang, replacements)\n    return render_template(\n        \"register.html\",\n        server_name=server_name,\n        pw_length=pw_length,\n        uname_regex=uname_regex,\n        uname_regex_inv=uname_regex_inv,\n        client_redirect=config.config.client_redirect,\n        base_url=config.config.base_url,\n        translations=translations,\n    )\n\n\ndef create_account_from_form(form):\n    # remove sigil and the domain from the username\n    username = form.username.data.rsplit(\":\")[0].split(\"@\")[-1]\n    logger.debug(\"creating account %s...\" % username)\n    # send account creation request to the hs\n    try:\n        account_data = create_account(\n            form.username.data,\n            form.password.data,\n            config.config.server_location,\n            config.config.registration_shared_secret,\n        )\n    except exceptions.ConnectionError:\n        logger.error(\n            \"can not connect to %s\" % config.config.server_location,\n            exc_info=True,\n        )\n        abort(500)\n    except exceptions.HTTPError as e:\n        resp = e.response\n        error = resp.json()\n        status_code = resp.status_code\n        if status_code == 404:\n            logger.error(\"no HS found at %s\" % config.config.server_location)\n        elif status_code == 403:\n            logger.error(\"wrong shared registration secret or not enabled\")\n        elif status_code == 400:\n            # most likely this should only be triggered if a userid\n            # is already in use\n            return make_response(jsonify(error), 400)\n        else:\n            logger.error(\"failure communicating with HS\", exc_info=True)\n        abort(500)\n\n    logger.debug(\"using token %s\" % form.token.data)\n    ips = \", \".join(get_request_ips(request)) if config.config.ip_logging else False\n    tokens.tokens.use(form.token.data, ips)\n\n    logger.debug(\"account creation succeded!\")\n    return jsonify(\n        access_token=account_data[\"access_token\"],\n        home_server=account_data[\"home_server\"],\n        user_id=account_data[\"user_id\"],\n        status=\"success\",\n        status_code=200,\n    )\n\n\ndef get_token(token):\n    if tokens.tokens.get_token(token):\n        return jsonify(tokens.tokens.get_token(token).toDict())\n    resp = {\"errcode\": \"MR_TOKEN_NOT_FOUND\", \"error\": \"token does not exist\"}\n    return make_response(jsonify(resp), 404)\n\n\ndef get_tokens():\n    return jsonify(tokens.tokens.toList())\n\n\ndef create_token(data):\n    if not data:\n        resp = {\n            \"errcode\": \"MR_BAD_USER_REQUEST\",\n            \"error\": \"no data was sent\",\n        }\n        return make_response(jsonify(resp), 400)\n\n    max_usage = False\n    expiration_date = None\n    try:\n\n        if \"expiration_date\" in data and data[\"expiration_date\"] is not None:\n            expiration_date = datetime.fromisoformat(data[\"expiration_date\"])\n        if \"max_usage\" in data:\n            max_usage = data[\"max_usage\"]\n        token = tokens.tokens.new(expiration_date=expiration_date, max_usage=max_usage)\n    except ValueError:\n        resp = {\n            \"errcode\": \"MR_BAD_DATE_FORMAT\",\n            \"error\": \"date wasn't in YYYY-MM-DD format\",\n        }\n        return make_response(jsonify(resp), 400)\n    return jsonify(token.toDict())\n\n\ndef update_token(token, data):\n    if \"ips\" in data or \"active\" in data or \"name\" in data:\n        resp = {\n            \"errcode\": \"MR_BAD_USER_REQUEST\",\n            \"error\": \"you're not allowed to change this property\",\n        }\n        return make_response(jsonify(resp), 400)\n    if tokens.tokens.update(token, data):\n        return jsonify(tokens.tokens.get_token(token).toDict())\n\n    resp = {\"errcode\": \"MR_TOKEN_NOT_FOUND\", \"error\": \"token does not exist\"}\n    return make_response(jsonify(resp), 404)\n\n\ndef delete_token(token):\n    if not tokens.tokens.get_token(token):\n        resp = {\"errcode\": \"MR_TOKEN_NOT_FOUND\", \"error\": \"token does not exist\"}\n        return (jsonify(resp), 404)\n    if tokens.tokens.delete(token):\n        resp = {\"success\": \"true\"}\n        return make_response(jsonify(resp), 200)\n\n    resp = {\"success\": \"false\"}\n    return make_response(jsonify(resp), 500)\n\n\n@healthcheck.route(\"/health\")\ndef health():\n    return make_response(\"OK\", 200)\n\n\n@api.route(\"/api/version\")\n@auth.login_required\ndef version():\n    with open(os.path.join(__location__, \"__init__.py\"), \"r\") as file:\n        version_file = file.read()\n        version_match = re.search(\n            r\"^__version__ = ['\\\"]([^'\\\"]*)['\\\"]\", version_file, re.M\n        )\n        resp = {\"version\": version_match.group(1)}\n        return make_response(jsonify(resp), 200)\n\n\n@api.route(\"/api/token\", methods=[\"GET\", \"POST\"])\n@auth.login_required\ndef token():\n    tokens.tokens.load()\n    if request.method == \"GET\":\n        return get_tokens()\n    elif request.method == \"POST\":\n        return create_token(request.get_json())\n\n    resp = {\"errcode\": \"MR_BAD_USER_REQUEST\", \"error\": \"malformed request\"}\n    return make_response(jsonify(resp), 400)\n\n\n@api.route(\"/api/token/<token>\", methods=[\"GET\", \"PATCH\", \"DELETE\"])\n@auth.login_required\ndef token_status(token):\n    tokens.tokens.load()\n    data = False\n    if request.method == \"GET\":\n        return get_token(token)\n\n    elif request.method == \"PATCH\":\n        return update_token(token, request.get_json())\n\n    elif request.method == \"DELETE\":\n        return delete_token(token)\n\n    resp = {\"errcode\": \"MR_BAD_USER_REQUEST\", \"error\": \"malformed request\"}\n    return make_response(jsonify(resp), 400)\n"
  },
  {
    "path": "matrix_registration/app.py",
    "content": "import json\nimport logging\nimport logging.config\nimport os\n\nimport click\nfrom flask import Flask\nfrom flask.cli import FlaskGroup, pass_script_info\nfrom flask_cors import CORS\nfrom waitress import serve\n\nfrom . import config\nfrom . import tokens\nfrom .limiter import limiter\nfrom .tokens import db\n\n\ndef create_app(testing=False):\n    app = Flask(__name__)\n    app.testing = testing\n\n    with app.app_context():\n        from .api import api, healthcheck\n\n        app.register_blueprint(api)\n        app.register_blueprint(healthcheck)\n\n    limiter.init_app(app)\n    return app\n\n\n@click.group(\n    cls=FlaskGroup,\n    add_default_commands=False,\n    create_app=create_app,\n    context_settings=dict(help_option_names=[\"-h\", \"--help\"]),\n)\n@click.option(\"--config-path\", help=\"specifies the config file to be used\")\n@pass_script_info\ndef cli(info, config_path):\n    \"\"\"a token based matrix registration app\"\"\"\n    config.config = config.Config(path=config_path)\n    logging.config.dictConfig(config.config.logging)\n    app = info.load_app()\n    with app.app_context():\n        app.config.from_mapping(\n            SQLALCHEMY_DATABASE_URI=config.config.db.format(cwd=f\"{os.getcwd()}\"),\n            SQLALCHEMY_TRACK_MODIFICATIONS=False,\n        )\n        db.init_app(app)\n        db.create_all()\n        tokens.tokens = tokens.Tokens()\n\n\n@cli.command(\"serve\", help=\"start api server\")\n@pass_script_info\ndef run_server(info):\n    app = info.load_app()\n    if config.config.allow_cors:\n        CORS(app)\n    serve(\n        app,\n        host=config.config.host,\n        port=config.config.port,\n        url_prefix=config.config.base_url,\n    )\n\n\n@cli.command(\"generate\", help=\"generate new token\")\n@click.option(\"-m\", \"--maximum\", default=0, help=\"times token can be used\")\n@click.option(\n    \"-e\",\n    \"--expires\",\n    type=click.DateTime(formats=[\"%Y-%m-%d\"]),\n    default=None,\n    help=\"expire date: in ISO-8601 format (YYYY-MM-DD)\",\n)\ndef generate_token(maximum, expires):\n    token = tokens.tokens.new(expiration_date=expires, max_usage=maximum)\n    print(token.name)\n\n\n@cli.command(\"status\", help=\"view status or disable\")\n@click.option(\"-s\", \"--status\", default=None, help=\"token status\")\n@click.option(\"-l\", \"--list\", is_flag=True, help=\"list tokens\")\n@click.option(\"-d\", \"--disable\", default=None, help=\"disable token\")\ndef status_token(status, list, disable):\n    if disable:\n        if tokens.tokens.disable(disable):\n            print(\"Token disabled\")\n        else:\n            print(\"Token couldn't be disabled\")\n    elif status:\n        token = tokens.tokens.get_token(status)\n        if token:\n            print(f\"This token is{' ' if token.active() else ' not '}valid\")\n            print(json.dumps(token.toDict(), indent=2))\n        else:\n            print(\"No token with that name\")\n    elif list:\n        print(tokens.tokens)\n"
  },
  {
    "path": "matrix_registration/config.py",
    "content": "# Standard library imports...\n# from collections import namedtuple\nimport logging\nimport os\nimport sys\n\n# Third-party imports...\nimport yaml\nfrom jsonschema import validate, ValidationError\n\n# Local imports...\nfrom .constants import (\n    CONFIG_SCHEMA_PATH,\n    CONFIG_DIR1,\n    CONFIG_DIR2,\n    CONFIG_DIR3,\n    CONFIG_DIR4,\n    CONFIG_DIR5,\n)\n\nCONFIG_DIRS = [CONFIG_DIR1, CONFIG_DIR2, CONFIG_DIR3, CONFIG_DIR4, CONFIG_DIR5]\n\nCONFIG_SAMPLE_NAME = \"config.sample.yaml\"\nCONFIG_NAME = \"config.yaml\"\nlogger = logging.getLogger(__name__)\n\n\nclass Config:\n    \"\"\"\n    Config\n\n    loads a dict or a yaml file to be accessible by all files in the module\n    \"\"\"\n\n    def __init__(self, path=None, data=None):\n        self.secrets_dir = os.getenv(\"CREDENTIALS_DIRECTORY\")\n        self.data = data\n        self.path = path\n        self.default = True\n\n        self.load()\n        if self.secrets_dir:\n            self.load_secrets()\n        self.apply_options()\n\n    def load(self):\n        \"\"\"\n        loads the options\n        \"\"\"\n        logger.debug(\"loading config...\")\n        if self.data:\n            logger.debug(\"from dict...\")\n            config_default = False\n            return\n\n        logger.debug(\"from file...\")\n        self.load_from_file()\n\n    def load_from_file(self):\n        \"\"\"\n        loads the options from a file\n        \"\"\"\n        options = None\n        if not self.check_config_locations():\n            sys.exit(\"could not find any configuration file!\")\n        logger.debug(f\"config found!\")\n\n        try:\n            with open(self.path, \"r\") as stream:\n                options = yaml.load(stream, Loader=yaml.SafeLoader)\n            with open(CONFIG_SCHEMA_PATH, \"r\") as schemafile:\n                validate(options, yaml.safe_load(schemafile))\n        except ValidationError as e:\n            sys.exit(\n                \"Check you config and update it to the newest version! Do you have missing fields in your config.yaml?\\n\\nTraceback:\\n\"\n                + str(e)\n            )\n        except yaml.YAMLError as e:\n            sys.exit(\"Invalid YAML Syntax\\n\\nTraceback:\\n\" + str(e))\n        except IOError as e:\n            sys.exit(e)\n\n        if not options:\n            sys.exit(\"could not read file\")\n\n        if self.default:\n            # ask for options that should not be set to default\n            options = self.ask_for_options(options)\n\n        self.data = options\n\n    def load_secrets(self):\n        \"\"\"\n        loads secret options, see https://systemd.io/CREDENTIALS/\n        \"\"\"\n        with open(f\"{self.secrets_dir}/secrets\") as file:\n            for line in file:\n                try:\n                    k, v = line.lower().split(\"=\")\n                except NameError:\n                    logger.error(\n                        f'secret \"{line}\" in wrong format, please use \"key=value\"'\n                    )\n                setattr(self, k.strip(), v.strip())\n\n    def check_config_locations(self):\n        \"\"\"\n        checks multiple locations for the config or config sample file\n        \"\"\"\n        if self.path:\n            logger.debug(f\"checking {self.path} ...\")\n            if os.path.isfile(self.path):\n                self.default = False\n                return True\n            else:\n                sys.exit(\"no configuration file at specified location\")\n\n        # check possible locations for config file\n        for directory in CONFIG_DIRS:\n            logger.debug(f\"checking {directory} ...\")\n            path = directory + CONFIG_SAMPLE_NAME\n            if os.path.isfile(path):\n                self.path = path\n                self.default = False\n                return True\n\n        # no config exists, use sample config instead\n        # check typical installation dirs for sample configs\n        for directory in CONFIG_DIRS:\n            path = directory + CONFIG_SAMPLE_NAME\n            if os.path.isfile(directory + CONFIG_SAMPLE_NAME):\n                self.path = path\n                self.config_exists = True\n                return True\n\n        return False\n\n    def apply_options(self):\n        \"\"\"\n        applies options to the config object\n        \"\"\"\n        logger.debug(\"applying options...\")\n        # recusively set dictionary to class properties\n        for k, v in self.data.items():\n            setattr(self, k, v)\n\n    def ask_for_options(self, sample_options):\n        \"\"\"\n        asks the user how to set the essential options\n\n        Parameters\n        ----------\n        arg1 : dict\n            with default values\n        \"\"\"\n        # important keys that need to be changed\n        keys = [\"server_location\", \"server_name\", \"port\", \"registration_shared_secret\"]\n        for key in keys:\n            temp = sample_options[key]\n            sample_options[key] = input(\"enter {}, e.g. {}\\n\".format(key, temp))\n            if not sample_options[key].strip():\n                sample_options[key] = temp\n\n        return sample_options\n\n        # write to config file\n        directory = os.path.dirname(os.path.realpath(self.path))\n        new_path = f\"{directory}/{CONFIG_NAME}\"\n        with open(new_path, \"w\") as stream:\n            yaml.dump(self.data, stream, default_flow_style=False)\n            print(f'config file written to \"{os.path.relpath(new_path)}\"')\n            print()\n\n    def update(self, data):\n        \"\"\"\n        resets all options and loads the new config\n\n        Parameters\n        ----------\n        arg1 : dict or path to config file\n        \"\"\"\n        logger.debug(\"updating config...\")\n        self.data = data\n        self.path = None\n        self.load()\n        if self.secrets_dir:\n            self.load_secrets()\n        self.apply_options()\n        logger.debug(\"config updated!\")\n\n\nconfig = None\n"
  },
  {
    "path": "matrix_registration/config.schema.json",
    "content": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"server_location\": {\n      \"type\": \"string\",\n      \"format\": \"uri\",\n      \"pattern\": \"^https?://\"\n    },\n    \"server_name\": {\n      \"type\": \"string\"\n    },\n    \"registration_shared_secret\": {\n      \"type\": \"string\"\n    },\n    \"admin_api_shared_secret\": {\n      \"type\": \"string\"\n    },\n    \"base_url\": {\n      \"type\": \"string\"\n    },\n    \"client_redirect\": {\n      \"type\": \"string\"\n    },\n    \"client_logo\": {\n      \"type\": \"string\"\n    },\n    \"db\": {\n      \"type\": \"string\"\n    },\n    \"host\": {\n      \"type\": \"string\"\n    },\n    \"port\": {\n      \"oneOf\": [\n        {\"type\": \"integer\"},\n        {\"type\": \"string\", \"pattern\": \"^/d+$\"}\n      ]\n    },\n    \"rate_limit\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"allow_cors\": {\n      \"type\": \"boolean\"\n    },\n    \"ip_logging\": {\n      \"type\": \"boolean\"\n    },\n    \"logging\": {\n      \"type\": \"object\"\n    },\n    \"password\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"min_length\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"min_length\"\n      ]\n    },\n    \"username\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"validation_regex\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"invalidation_regex\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"required\": [\n        \"validation_regex\",\n        \"invalidation_regex\"\n      ]\n    }\n  },\n  \"required\": [\n    \"server_location\",\n    \"server_name\",\n    \"registration_shared_secret\",\n    \"admin_api_shared_secret\",\n    \"base_url\",\n    \"client_redirect\",\n    \"client_logo\",\n    \"db\",\n    \"host\",\n    \"port\",\n    \"rate_limit\",\n    \"allow_cors\",\n    \"ip_logging\",\n    \"logging\",\n    \"password\",\n    \"username\"\n  ]\n}\n"
  },
  {
    "path": "matrix_registration/constants.py",
    "content": "# Standard library imports...\nimport os\nimport site\nimport sys\n\n# Third-party imports...\nfrom appdirs import user_config_dir\n\n__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))\nWORD_LIST_PATH = os.path.join(__location__, \"wordlist.txt\")\nCONFIG_SCHEMA_PATH = os.path.join(__location__, \"config.schema.json\")\n# first check in current working dir\nCONFIG_DIR1 = os.path.join(os.getcwd() + \"/\")\nCONFIG_DIR2 = os.path.join(os.getcwd() + \"/config/\")\n# then check in XDG_CONFIG_HOME\nCONFIG_DIR3 = os.path.join(user_config_dir(\"matrix-registration\") + \"/\")\n# check at installed location\nCONFIG_DIR4 = os.path.join(__location__, \"../\")\nCONFIG_DIR5 = os.path.join(sys.prefix, \"config/\")\n"
  },
  {
    "path": "matrix_registration/limiter.py",
    "content": "from flask import request\nfrom flask_limiter import Limiter\n\nfrom . import config\n\n\ndef get_real_user_ip() -> str:\n    \"\"\"ratelimit the users original ip instead of (optional) reverse proxy\"\"\"\n    return next(iter(request.headers.getlist(\"X-Forwarded-For\")), request.remote_addr)\n\n\ndef get_default_rate_limit() -> str:\n    \"\"\"return limit_string\"\"\"\n    return \"; \".join(config.config.rate_limit)\n\n\nlimiter = Limiter(key_func=get_real_user_ip)\n"
  },
  {
    "path": "matrix_registration/matrix_api.py",
    "content": "# Standard library imports...\nimport hashlib\nimport hmac\nimport requests\n\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n\ndef create_account(\n    user, password, server_location, shared_secret, admin=False, user_type=None\n):\n    \"\"\"\n    creates account\n    https://github.com/matrix-org/synapse/blob/master/synapse/_scripts/register_new_matrix_user.py\n\n    Parameters\n    ----------\n    arg1 : str\n        local part of the new user\n    arg2 : str\n        password\n    arg3 : str\n        url to homeserver\n    arg4 : str\n        Registration Shared Secret as set in the homeserver.yaml\n    arg5 : bool\n        register new user as an admin.\n    Raises\n    -------\n    requests.exceptions.ConnectionError:\n        can't connect to homeserver\n    requests.exceptions.HTTPError:\n        something with the communciation to the homeserver failed\n    \"\"\"\n    nonce = _get_nonce(server_location)\n\n    mac = hmac.new(key=shared_secret.encode(\"utf8\"), digestmod=hashlib.sha1)\n\n    mac.update(nonce.encode(\"utf8\"))\n    mac.update(b\"\\x00\")\n    mac.update(user.encode(\"utf8\"))\n    mac.update(b\"\\x00\")\n    mac.update(password.encode(\"utf8\"))\n    mac.update(b\"\\x00\")\n    mac.update(b\"admin\" if admin else b\"notadmin\")\n    if user_type:\n        mac.update(b\"\\x00\")\n        mac.update(user_type.encode(\"utf8\"))\n\n    mac = mac.hexdigest()\n\n    data = {\n        \"nonce\": nonce,\n        \"username\": user,\n        \"password\": password,\n        \"mac\": mac,\n        \"admin\": admin,\n        \"user_type\": user_type,\n    }\n\n    server_location = server_location.rstrip(\"/\")\n\n    r = requests.post(\"%s/_synapse/admin/v1/register\" % (server_location), json=data)\n    r.raise_for_status()\n    return r.json()\n\n\ndef _get_nonce(server_location):\n    r = requests.get(\"%s/_synapse/admin/v1/register\" % (server_location))\n    r.raise_for_status()\n    return r.json()[\"nonce\"]\n"
  },
  {
    "path": "matrix_registration/static/css/style.css",
    "content": "html,\nbody {\n  height: 100%;\n  margin: 0;\n  font-family: 'Nunito', sans-serif;\n}\n\nbody {\n  background-size: cover;\n  background-attachment: fixed;\n  overflow: hidden;\n}\n\nh1 {\n  font-size: 1.3em;\n}\n\narticle {\n  color: white;\n}\n\na:link,\na:visited {\n  color: #038db3 !important;\n}\n\nform {\n  width: 320px;\n  margin: 45px auto;\n}\n\ntextarea {\n  resize: none;\n}\n\ninput,\ntextarea {\n  background: none;\n  color: white;\n  font-size: 18px;\n  padding: 10px 10px 10px 5px;\n  display: block;\n  width: 320px;\n  border: none;\n  border-radius: 0;\n  border-bottom: 1px solid white;\n}\n\ninput:focus,\ntextarea:focus {\n  outline: none;\n}\n\ninput:focus~label,\ninput:not(:placeholder-shown)~label,\ntextarea:focus~label,\ntextarea:valid~label {\n  top: -14px;\n  font-size: 12px;\n  color: #03b381;\n}\n\ninput:focus~.bar:before,\ntextarea:focus~.bar:before {\n  width: 320px;\n}\n\ninput[type=\"password\"] {\n  letter-spacing: 0.3em;\n}\n\ninput:invalid {\n  box-shadow: none;\n}\n\ninput:invalid~.bar:before {\n  background: #038db3;\n}\n\ninput:invalid~label {\n  color: #038db3;\n}\n\ninput[type=\"submit\"] {\n  cursor: pointer;\n}\n\nlabel {\n  color: white;\n  font-size: 16px;\n  font-weight: normal;\n  position: absolute;\n  pointer-events: none;\n  left: 5px;\n  top: 10px;\n  transition: 300ms ease all;\n}\n\n*,\n:before,\n:after {\n  box-sizing: border-box;\n}\n\n.center {\n  text-align: center;\n  margin-top: 2em;\n}\n.hidden {\n  visibility: hidden;\n  opacity: 0;\n}\n\n.group {\n  position: relative;\n  margin: 45px 0;\n}\n\n.bar {\n  position: relative;\n  display: block;\n  width: 320px;\n}\n\n.bar:before {\n  content: '';\n  height: 2px;\n  width: 0;\n  bottom: 0px;\n  position: absolute;\n  background: #03b381;\n  transition: 300ms ease all;\n  left: 0%;\n}\n\n.btn {\n  background: white;\n  color: black;\n  border: none;\n  padding: 10px 20px;\n  border-radius: 3px;\n  letter-spacing: 0.06em;\n  text-transform: uppercase;\n  text-decoration: none;\n  outline: none;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);\n  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n.btn:hover {\n  color: black;\n  box-shadow: 0 7px 14px rgba(0, 0, 0, 0.18), 0 5px 5px rgba(0, 0, 0, 0.12);\n}\n\n.btn.btn-submit {\n  background: #03b381;\n  color: #bce0fb;\n}\n\n.btn.btn-submit:hover {\n  background: #03b372;\n  color: #deeffd;\n}\n\n.btn-box {\n  text-align: center;\n  margin: 50px 0;\n}\n\n.info {\n  z-index: 2;\n  position: absolute;\n  bottom: .5vh;\n  right: 1vw;\n  text-align: left;\n  color: grey;\n  font-size: 0.8em;\n  opacity: 0.1;\n  transition: opacity 0.5s ease;\n}\n\n.info:hover {\n  opacity: 1;\n}\n\n.info a {\n  color: cyan;\n}\n\n.widget {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  border: 0px solid;\n  border-radius: 5px;\n  overflow: hidden;\n  background-color: #1f1f1f;\n  z-index: 1;\n  box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.5);\n}\n\n.widget::before {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: -1;\n  width: 100%;\n  height: 100%;\n  background-attachment: fixed;\n  background-size: cover;\n  opacity: 0.20;\n  content: \"\";\n}\n\n.blur:before {\n  content: \"\";\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  background: inherit;\n  z-index: -1;\n  transform: scale(1.03);\n  filter: blur(10px);\n}\n\n.register {\n  margin-left: -15em;\n  margin-top: -20em;\n  width: 30em;\n  height: 40em;\n}\n\n.modal {\n  margin-left: -12.5em;\n  margin-top: -7.5em;\n  width: 25em;\n  background-color: #f7f7f7;\n  transition: visibility .3s, opacity .3s linear;\n}\n\n.modal article {\n  margin-top: -5em;\n}\n\n.modal article,\n.modal p,\n.modal h2,\n.modal h3 {\n  color: #1f1f1f;\n}\n\n.error {\n  color: #b30335 !important;\n}\n\n@media only screen and (max-width: 500px) {\n  .info {\n    bottom: -2vh;\n  }\n\n  .widget {\n    margin-top: -40vh;\n    margin-left: -45vw;\n    width: 90vw;\n    min-width: 20em;\n  }\n\n  .modal {\n    margin-top: -15vh;\n    margin-left: -35vw;\n    width: 70vw;\n    min-width: 15em;\n  }\n}\n\n@media only screen and (max-height: 768px) {\n  body {\n    overflow-y: visible;\n    padding-bottom: -90vh;\n  }\n\n  .blur:before {\n    filter: none;\n    transform: none;\n    padding-bottom: 50em;\n  }\n\n  .info {\n    float: right;\n    padding-top: 57em;\n    position: static;\n  }\n\n  .widget {\n    margin-top: -40vh;\n  }\n\n  .modal {\n    margin-top: -15vh;\n  }\n}"
  },
  {
    "path": "matrix_registration/static/fonts/NUNITO-LICENSE",
    "content": "Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "matrix_registration/templates/register.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width initial-scale=1.0\" />\n  <meta property=\"og:title\" content=\"{{ translations.server_registration }}\">\n  <meta property=\"og:site_name\" content=\"{{ server_name }}\">\n  <meta property=\"og:type\" content=\"website\" />\n  <meta name=\"og:description\" content=\"{{ translations.register_account }}\"/>\n  <meta name=\"og:image\" content=\"{{ url_for('static', filename='images/icon.png') }}\" />\n  <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"{{ url_for('static', filename='images/icon.png') }}\">\n  <link rel=\"icon\" type=\"image/png\" href=\"{{ url_for('static', filename='images/icon32x32.png') }}\" sizes=\"32x32\">\n  <link rel=\"preload\" as=\"image\" href=\"{{ url_for('static', filename='images/favicon.ico') }}\">\n  <link rel=\"shortcut icon\" href=\"{{ url_for('static', filename='images/favicon.ico') }}\">\n  <meta name=\"msapplication-TileImage\" content=\"{{ url_for('static', filename='images/tile.png') }}\">\n  <meta name=\"msapplication-TileColor\" content=\"#fff\">\n  <title>{{ translations.server_registration }}</title>\n  <!-- font designed by Vernon Adams, Cyreal -->\n  <!-- https://fonts.google.com/specimen/Nunito -->\n  <!-- licensed under SIL Open Font License, Version 1.1 -->\n  <link rel=\"preload\" as=\"font\" href=\"{{ url_for('static', filename='fonts/Nunito_400_Nunito_700.woff2') }}\" type=\"font/woff2\" crossorigin=\"anonymous\">\n  <link rel=\"preload\" as=\"stylesheet\" href=\"{{ url_for('static', filename='css/style.css') }}\">\n  <!-- valley cover by Designed by Jesús Roncero -->\n  <!-- https://www.flickr.com/golan -->\n  <!-- licensed under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/) -->\n  <link rel=\"preload\" as=\"image\" href=\"{{ url_for('static', filename='images/valley.jpg') }}\">\n  <style>\n    @font-face {\n      font-family: \"Nunito\";\n      src: local(\"Nunito\"), url(\"{{ url_for('static', filename='fonts/Nunito_400_Nunito_700.woff2') }}\");\n    }\n    body, .widget::before {\n      background-image: url(\"{{ url_for('static', filename='images/valley.jpg') }}\");\n    }\n  </style>\n  <link rel=\"stylesheet\" href=\"{{ url_for('static', filename='css/style.css') }}\">\n</head>\n\n<body class=\"blur\">\n  <article class=\"widget register\">\n    <div class=\"center\">\n      <header>\n        <h1>{{ translations.server_registration }}</h1>\n        <p>{{ translations.requires_token }}<br>\n          {{ translations.requires_username_and_password }}</p>\n      </header>\n      <section>\n        <form id=\"registration\" action=\"{{ base_url }}/register\" method=\"post\">\n          <div class=\"group\">\n            <input id=\"username\" name=\"username\" type=\"text\" placeholder=\" \"\n              pattern=\"^@?[a-zA-Z_\\-=\\.\\/0-9]+(:{{ server_name|replace('.', '\\.') }})?$\" \n              minlength=\"1\" maxlength=\"200\" required>\n            <span class=\"highlight\"></span>\n            <span class=\"bar\"></span>\n            <label for=\"username\">{{ translations.username }}</label>\n          </div>\n          <div class=\"group\">\n            <input id=\"password\" name=\"password\" type=\"password\" placeholder=\" \" required minlength=\"{{ pw_length }}\"\n              maxlength=\"128\">\n            <span class=\"highlight\"></span>\n            <span class=\"bar\"></span>\n            <label for=\"password\">{{ translations.password }}</label>\n          </div>\n          <div class=\"group\">\n            <input id=\"confirm_password\" name=\"confirm\" type=\"password\" placeholder=\" \" required>\n            <span class=\"highlight\"></span>\n            <span class=\"bar\"></span>\n            <label for=\"confirm_password\">{{ translations.confirm }}</label>\n          </div>\n          <div class=\"group\">\n            <input id=\"token\" name=\"token\" type=\"text\" placeholder=\" \" required pattern=\"^([A-Z][a-z]+)+$\">\n            <span class=\"highlight\"></span>\n            <span class=\"bar\"></span>\n            <label for=\"token\">{{ translations.token }}</label>\n          </div>\n          <div class=\"btn-box\">\n            <input class=\"btn btn-submit\" type=\"submit\" value=\"{{ translations.register }}\">\n          </div>\n        </form>\n      </section>\n    </div>\n  </article>\n  <article id=\"success\" class=\"widget modal hidden\">\n    <div class=\"center\">\n      <header>\n        <h2 id=\"welcome\"></h2>\n      </header>\n      <section>\n        <p> {{ translations.click_to_login }}</p>\n        <h3><a href=\"{{ client_redirect }}\"><img src=\"{{ base_url }}/static/replace/images/element-logo.png\" height=\"100px\"></a></h3>\n        <p>{{ translations.choose_client }} <a href=\"https://matrix.org/docs/projects/clients-matrix\"\n            a>https://matrix.org/docs/projects/clients-matrix</a></p>\n      </section>\n    </div>\n  </article>\n  <article id=\"error\" class=\"widget modal hidden\">\n    <div class=\"center\">\n      <header>\n        <h2>{{ translations.error }}</h2>\n      </header>\n      <section>\n        <p>{{ translations.error_long }}</p>\n        <h3 id=\"error_message\" class=\"error\"></h3>\n        <p id=\"error_dialog\"></p>\n      </section>\n    </div>\n  </article>\n  <footer class=\"info\">\n    <p>Cover photo by: <a href=\"https://www.flickr.com/golan\" target=\"_blank\">Jesús Roncero</a>,<br>\n      used under the terms of <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\" target=\"_blank\">CC-BY-SA\n        4.0</a>. No warranties are given.\n    </p>\n  </footer>\n\n  <script>\n    // all javascript here is optional, the registration form works fine without\n    /* \n    What this script does:\n      - confirm password validator needs javascript, otherwise always valid as long as not empty\n      - set token with ?token query parameter\n      - set custom validity messages\n    */\n\n    // see https://stackoverflow.com/a/3028037\n    function hideOnClickOutside(element) {\n      const outsideClickListener = event => {\n        if (!element.contains(event.target) && isVisible(\n            element)) {\n          element.classList.add(\"hidden\")\n          removeClickListener()\n        }\n      }\n\n      const removeClickListener = () => {\n        document.removeEventListener(\"click\", outsideClickListener)\n      }\n      document.addEventListener(\"click\", outsideClickListener)\n    }\n\n    const isVisible = elem => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length)\n\n    // set token input to \"?token=\" query parameter\n    const urlParams = new URLSearchParams(window.location.search)\n    document.getElementById(\"token\").value = urlParams.get(\"token\")\n\n    // set \"?lang=\" parameter to user lang\n    const userLang = navigator.language || navigator.userLanguage\n    if (!urlParams.has(\"lang\")) { \n      urlParams.append(\"lang\", userLang)\n      window.history.replaceState({}, '', `${location.pathname}?${urlParams}`);\n    }\n\n    // html5 validators\n    var username = document.getElementById(\"username\")\n    var password = document.getElementById(\"password\")\n    var confirm_password = document.getElementById(\"confirm_password\")\n    var token = document.getElementById(\"token\")\n\n    username.addEventListener(\"input\", function (event) {\n      if (username.validity.patternMismatch) {\n        username.setCustomValidity(\"{{ translations.username_format }}\")\n      } else {\n        var uname = username.value.replace(/^@/,'').split(\":\")[0]\n        {% for each in uname_regex if each %}\n          if (uname.match(/{{ each }}/)==null) {\n            username.setCustomValidity(\"{{ translations.username_error }}\")\n            return\n          }\n        {% endfor %}\n        {% for each in uname_regex_inv if each %}\n          if (uname.match(/{{ each }}/)!=null) {\n            username.setCustomValidity(\"{{ translations.username_error }}\")\n            return\n          }\n        {% endfor %}\n        username.setCustomValidity(\"\")\n      }\n    })\n\n    token.addEventListener(\"input\", function (event) {\n      if (token.validity.patternMismatch) {\n        token.setCustomValidity(\"{{ translations.case_sensitive }}\")\n      } else {\n        token.setCustomValidity(\"\")\n      }\n    })\n\n    password.addEventListener(\"input\", function (event) {\n      if (password.validity.tooShort) {\n        password.setCustomValidity('{{ translations.password_too_short }}')\n      } else {\n        password.setCustomValidity(\"\")\n      }\n    })\n\n    function validatePassword() {\n      if (password.value != confirm_password.value) {\n        confirm_password.setCustomValidity(\"{{ translations.password_do_not_match }}\")\n      } else {\n        confirm_password.setCustomValidity(\"\")\n      }\n    }\n\n    password.onchange = validatePassword\n    confirm_password.onkeyup = validatePassword\n\n    function showError(message, dialog) {\n      document.getElementById(\"error_message\").innerHTML = message\n      document.getElementById(\"error_dialog\").innerHTML = dialog\n      let error = document.getElementById(\"error\")\n      error.classList.remove(\"hidden\")\n      hideOnClickOutside(error)\n    }\n\n    // hijack the submit button to display the json response in a neat modal\n    var form = document.getElementById(\"registration\")\n\n    function sendData() {\n      let XHR = new XMLHttpRequest()\n\n      // Bind the FormData object and the form element\n      let FD = new FormData(form)\n\n      // Define what happens on successful data submission\n      XHR.addEventListener(\"load\", function (event) {\n        console.log(XHR.responseText)\n        let response = JSON.parse(XHR.responseText)\n        try {\n          console.log(response)\n        } catch (e) {\n          if (e instanceof SyntaxError) {\n            showError(\"{{ translations.internal_error }}\", \"{{ translations.contact }}\")\n            return\n          }\n        }\n        if (\"errcode\" in response) {\n          if (response[\"errcode\"] == \"MR_BAD_USER_REQUEST\") {\n            if (\"token\" in response[\"error\"]) {\n              showError(\" {{ translations.token_error }} \", response[\"error\"][\"token\"][0])\n            } else if (\"password\" in response[\"error\"]) {\n              showError(\"{{ translations.password_error }}\", response[\"error\"][\"password\"][0])\n            } else if (\"username\" in response[\"error\"]) {\n              showError(\"{{ translations.username_error }}\", response[\"error\"][\"username\"][0])\n            }\n            return\n          } else {\n            showError(\"{{ translations.homeserver_error }}\", response[\"error\"])\n          }\n        } else {\n          document.getElementById(\"welcome\").innerHTML = \"{{ translations.welcome }} \"  + response['user_id']\n          document.getElementById(\"success\").classList.remove(\"hidden\")\n        }\n\n      })\n\n      // Define what happens in case of error\n      XHR.addEventListener(\"error\", function (event) {\n        showError(\"{{ translations.internal_error }}\", \"{{ translations.contact }}\")\n      })\n\n      // Set up our request\n      XHR.open(\"POST\", \"{{ base_url }}/register\")\n\n      // The data sent is what the user provided in the form\n      XHR.send(FD)\n    }\n\n    // take over its submit event.\n    form.addEventListener(\"submit\", function (event) {\n      event.preventDefault()\n\n      sendData()\n    })\n  </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "matrix_registration/tokens.py",
    "content": "# Standard library imports...\nfrom datetime import datetime\nimport logging\nimport random\n\nfrom flask_sqlalchemy import SQLAlchemy\nfrom sqlalchemy import (\n    exc,\n    Table,\n    Column,\n    Integer,\n    String,\n    Boolean,\n    DateTime,\n    ForeignKey,\n)\nfrom sqlalchemy.orm import relationship\n\n# Local imports...\nfrom .constants import WORD_LIST_PATH\n\n\nlogger = logging.getLogger(__name__)\n\ndb = SQLAlchemy()\nsession = db.session\n\n\ndef random_readable_string(length=3, wordlist=WORD_LIST_PATH):\n    with open(wordlist) as f:\n        lines = f.read().splitlines()\n        string = \"\"\n        for _ in range(length):\n            string += random.choice(lines).title()\n    return string\n\n\nassociation_table = Table(\n    \"association\",\n    db.Model.metadata,\n    Column(\"ips\", Integer, ForeignKey(\"ips.id\"), primary_key=True),\n    Column(\"tokens\", String(255), ForeignKey(\"tokens.name\"), primary_key=True),\n)\n\n\nclass IP(db.Model):\n    __tablename__ = \"ips\"\n    id = Column(Integer, primary_key=True)\n    address = Column(String(255))\n\n    def __repr__(self):\n        return self.address\n\n\nclass Token(db.Model):\n    __tablename__ = \"tokens\"\n    name = Column(String(255), primary_key=True)\n    expiration_date = Column(DateTime, nullable=True)\n    max_usage = Column(Integer, default=1)\n    used = Column(Integer, default=0)\n    disabled = Column(Boolean, default=False)\n    ips = relationship(\n        \"IP\",\n        secondary=association_table,\n        lazy=\"subquery\",\n        backref=db.backref(\"pages\", lazy=True),\n    )\n\n    def __init__(self, **kwargs):\n        super(Token, self).__init__(**kwargs)\n        if not self.name:\n            self.name = random_readable_string()\n        if not self.used:\n            self.used = 0\n        if not self.max_usage:\n            self.max_usage = 0\n\n    def __repr__(self):\n        return self.name\n\n    def toDict(self):\n        _token = {\n            \"name\": self.name,\n            \"used\": self.used,\n            \"expiration_date\": str(self.expiration_date)\n            if self.expiration_date\n            else None,\n            \"max_usage\": self.max_usage,\n            \"ips\": list(map(lambda x: x.address, self.ips)),\n            \"disabled\": bool(self.disabled),\n            \"active\": self.active(),\n        }\n        return _token\n\n    def active(self):\n        expired = False\n        if self.expiration_date:\n            expired = self.expiration_date < datetime.now()\n        used = self.max_usage != 0 and self.max_usage <= self.used\n\n        return (not expired) and (not used) and (not self.disabled)\n\n    def use(self, ip_address=False):\n        if self.active():\n            self.used += 1\n            if ip_address:\n                self.ips.append(IP(address=ip_address))\n            return True\n        return False\n\n    def disable(self):\n        if not self.disabled:\n            self.disabled = True\n            return True\n        return False\n\n\nclass Tokens:\n    def __init__(self):\n        self.tokens = {}\n\n        self.load()\n\n    def __repr__(self):\n        result = \"\"\n        for tokens_key in self.tokens:\n            result += \"%s, \" % tokens_key\n        return result[:-2]\n\n    def toList(self):\n        _tokens = []\n        for tokens_key in self.tokens:\n            _tokens.append(self.tokens[tokens_key].toDict())\n        return _tokens\n\n    def load(self):\n        logger.debug(\"loading tokens from ..\")\n        self.tokens = {}\n        for token in Token.query.all():\n            logger.debug(token)\n            self.tokens[token.name] = token\n\n        logger.debug(\"token loaded!\")\n\n    def get_token(self, token_name):\n        logger.debug(\"getting token by name: %s\" % token_name)\n        try:\n            token = Token.query.filter_by(name=token_name).first()\n        except KeyError:\n            return False\n        return token\n\n    def active(self, token_name):\n        logger.debug('checking if \"%s\" is active' % token_name)\n        token = self.get_token(token_name)\n        if token:\n            return token.active()\n        return False\n\n    def use(self, token_name, ip_address=False):\n        logger.debug(\"using token: %s\" % token_name)\n        token = self.get_token(token_name)\n        if token:\n            if token.use(ip_address):\n                session.commit()\n                return True\n        return False\n\n    def update(self, token_name, data):\n        logger.debug(\"updating token: %s\" % token_name)\n        token = self.get_token(token_name)\n        if not token:\n            return False\n        if \"expiration_date\" in data:\n            token.expiration_date = data[\"expiration_date\"]\n        if \"max_usage\" in data:\n            token.max_usage = data[\"max_usage\"]\n        if \"used\" in data:\n            token.used = data[\"used\"]\n        if \"disabled\" in data:\n            token.disabled = data[\"disabled\"]\n        session.commit()\n        return True\n\n    def disable(self, token_name):\n        logger.debug(\"disabling token: %s\" % token_name)\n        token = self.get_token(token_name)\n        if token:\n            if token.disable():\n                session.commit()\n                return True\n        return False\n\n    def delete(self, token_name):\n        logger.debug(\"disabling token: %s\" % token_name)\n        try:\n            Token.query.filter_by(name=token_name).delete()\n            session.commit()\n        except exc.SQLAlchemyError as e:\n            logger.exception(e)\n            return False\n        return True\n\n    def new(self, expiration_date=None, max_usage=False):\n        logger.debug(\n            (\n                \"creating new token, with options: max_usage: {},\"\n                + \"expiration_dates: {}\"\n            ).format(max_usage, expiration_date)\n        )\n        token = Token(expiration_date=expiration_date, max_usage=max_usage)\n        self.tokens[token.name] = token\n        session.add(token)\n        session.commit()\n\n        return token\n\n\ntokens = None\n"
  },
  {
    "path": "matrix_registration/translation.py",
    "content": "import os\nimport re\n\nimport yaml\n\nfrom .constants import __location__\n\n\nreplace_pattern = re.compile(r\"{{\\s*(?P<name>.[a-zA-Z_\\-]+)\\s*}}\")\n\n\ndef get_translations(lang=\"en\", replacements={}):\n    default = _get_translations(replacements=replacements)\n    try:\n        selected = _get_translations(lang=lang, replacements=replacements)\n        return {**default, **selected}\n    except IOError:\n        return default\n\n\ndef _get_translations(lang=\"en\", replacements={}):\n    path = os.path.join(__location__, f\"translations/messages.{lang}.yaml\")\n\n    with open(path, \"r\") as stream:\n        translations = yaml.load(stream, Loader=yaml.SafeLoader)\n\n    interpolated_translations = {}\n    for key, value in translations[\"weblate\"].items():\n        match = re.search(replace_pattern, value)\n        while match:\n            value = value.replace(\n                match.group(0), str(replacements[match.group(\"name\")])\n            )\n            match = re.search(replace_pattern, value)\n\n        interpolated_translations[key] = value\n\n    return interpolated_translations\n"
  },
  {
    "path": "matrix_registration/translations/messages.de.yaml",
    "content": "weblate:\n  server_registration: \"{{ server_name }} Registrierung\"\n  register_account: \"Registriere einen Account auf {{ server_name }}\"\n  requires_token: \"Die Registrierung erfordert ein geheimes Token\"\n  requires_username_and_password: |\n    Für die Registrierung ist keine E-Mail erforderlich, nur ein Benutzername und ein Passwort, welches länger als {{ pw_length }} Zeichen ist.\n  username: \"Nutzername\"\n  password: \"Passwort\"\n  confirm: \"Bestätige\"\n  token: \"Token\"\n  register: \"registriere\"\n  click_to_login: \"Klicke hier um einzuloggen:\"\n  choose_client: \"oder wähle einen der vielen anderen Clienten hier:\"\n  username_format: \"Format: @username:{{ server_name }}\"\n  case_sensitive: \"Groß- und Kleinschreibung beachten, z.B.: SardineImpactReport\"\n  password_too_short: \"mindestens {{ pw_length }} Zeichen lang\"\n  password_do_not_match: \"Passwörter stimmen nicht überein\"\n  error: \"Fehler\"\n  error_long: \"Es gab einen Fehler währrend du registriert wurdest.\"\n  internal_error: \"Interner Server Fehler!\"\n  contact: \"Bitte kontaktieren sie Ihren Server Administrator über dies.\"\n  token_error: Token Fehler\"\n  password_error: \"Password Fehler\"\n  username_error: \"Nutzername Fehler\"\n  homeserver_error: \"Homeserver Fehler\"\n  welcome: \"Willkommen\"\n"
  },
  {
    "path": "matrix_registration/translations/messages.en.yaml",
    "content": "weblate:\n  server_registration: \"{{ server_name }} registration\"\n  register_account: \"register an account on {{ server_name }}\"\n  requires_token: \"the registration requires a secret token\"\n  requires_username_and_password: >\n    registration does not require an email, just a username and a password\n    that's longer than {{ pw_length }} characters.\n  username: \"Username\"\n  password: \"Password\"\n  confirm: \"Confirm\"\n  token: \"Token\"\n  register: \"register\"\n  click_to_login: \"Click here to login in:\"\n  choose_client: \"or choose one of the many other clients here:\"\n  username_format: \"format: @username:{{ server_name }}\"\n  case_sensitive: \"case-sensitive, e.g: SardineImpactReport\"\n  password_too_short: \"atleast {{ pw_length }} characters long\"\n  password_do_not_match: passwords don't match\n  error: \"Error\"\n  error_long: \"There was an error while trying to register you.\"\n  internal_error: \"Internal Server Error!\"\n  contact: \"Please contact the server admin about this.\"\n  token_error: \"Token Error\"\n  password_error: \"Password Error\"\n  username_error: \"Username Error\"\n  homeserver_error: \"Homeserver Error\"\n  welcome: \"Welcome\"\n"
  },
  {
    "path": "matrix_registration/translations/messages.pt_BR.yaml",
    "content": "weblate:\n  error_long: Ocorreu um erro enquanto tentávamos fazer seu registro.\n  register: registrar\n  confirm: Confirmar a senha\n  internal_error: Erro Interno no Servidor!\n  welcome: Bem-vindo\n  homeserver_error: Erro no Homeserver\n  username_error: Erro no Nome do Usuário\n  password_error: Erro na Senha\n  token_error: Erro no Token\n  contact: Por favor, contacte o administrador do servidor sobre esse assunto.\n  error: Erro\n  password_do_not_match: as senhas não são iguais\n  password_too_short: pelo menos {{ pw_length }} caracteres de comprimento\n  case_sensitive: 'maiúsculas e minúsculas, p.ex.: SardinhaImpactoRelatorio'\n  username_format: 'formato: @nome_do_usuário:{{ server_name }}'\n  choose_client: 'ou escolha algum dos outros clientes aqui:'\n  click_to_login: 'Clique aqui para logar em:'\n  token: Token\n  password: Senha\n  username: Nome do usuário\n  requires_username_and_password: \"o registro não requer endereço de email, apenas\\\n    \\ um nome de usuário e uma senha maior que {{ pw_length }} caracteres.\\n\"\n  requires_token: o registro necessita de um token secreto\n  register_account: registre uma conta no {{ server_name }}\n  server_registration: Registro no {{ server_name }}\n"
  },
  {
    "path": "matrix_registration/translations/messages.sv.yaml",
    "content": "weblate:\n  welcome: Välkommen\n  homeserver_error: Hemserverfel\n  username_error: Användarnamnsfel\n  password_error: Lösenordsfel\n  token_error: Token-fel\n  contact: Vänligen kontakta serveradministratören angående detta.\n  internal_error: Internt Serverfel!\n  error_long: Det uppstod ett fel när du skulle registreras.\n  error: Error\n  password_do_not_match: lösenorden matchar inte\n  password_too_short: åtminstone {{ pw_length }} symboler långt\n  case_sensitive: 'skiftlägeskänslighet, t.ex.: SardineImpactReport'\n  username_format: 'format: @användarnamn:{{ server_name }}'\n  choose_client: 'eller välj en av de många andra klienterna här:'\n  click_to_login: 'Klicka här för att logga in:'\n  register: registrera\n  token: Token\n  confirm: Bekräfta\n  password: Lösenord\n  username: Användarnamn\n  requires_username_and_password: \"registreringen kräver inte en mejladress, enbart\\\n    \\ ett användarnamn och ett lösenord som är längre än {{ pw_length }} tecken.\\n\"\n  requires_token: registreringen kräver en hemlig token\n  server_registration: '{{ server_name }} registrering'\n  register_account: registrera ett konto på {{ server_name }}\n"
  },
  {
    "path": "matrix_registration/translations/messages.zh_Hans.yaml",
    "content": "weblate:\n  requires_token: 注册需要一个秘密令牌\n  welcome: 欢迎\n  homeserver_error: 主服务器错误\n  username_error: 用户名错误\n  password_error: 密码错误\n  token_error: 令牌错误\n  contact: 请就此事与服务器管理员联系。\n  internal_error: 服务器内部错误！\n  error_long: 在为你注册时出现了错误。\n  error: 错误\n  password_do_not_match: 密码不匹配\n  password_too_short: 至少 {{ pw_length }} 个字符\n  case_sensitive: 大小写敏感，如 SardineImpactReport\n  username_format: 格式：@用户名:{{ server_name }}\n  choose_client: 或从这些其他客户端中选择：\n  click_to_login: 点此登陆：\n  register: 注册\n  token: 令牌\n  confirm: 确认密码\n  password: 密码\n  username: 用户名\n  requires_username_and_password: \"注册不需要电子邮箱，仅需用户名与长于{{ pw_length }}个字符的密码。\\n\"\n  register_account: 在 {{ server_name }} 注册账户\n  server_registration: '{{ server_name }} 注册'\n"
  },
  {
    "path": "matrix_registration/wordlist.txt",
    "content": "acrobat\nafrica\nalaska\nalbert\nalbino\nalbum\nalcohol\nalex\nalpha\namadeus\namanda\namazon\namerica\nanalog\nanimal\nantenna\nantonio\napollo\napril\naroma\nartist\naspirin\nathlete\natlas\nbanana\nbandit\nbanjo\nbikini\nbingo\nbonus\ncamera\ncanada\ncarbon\ncasino\ncatalog\ncinema\ncitizen\ncobra\ncomet\ncompact\ncomplex\ncontext\ncredit\ncritic\ncrystal\nculture\ndavid\ndelta\ndialog\ndiploma\ndoctor\ndomino\ndragon\ndrama\nextra\nfabric\nfinal\nfocus\nforum\ngalaxy\ngallery\nglobal\nharmony\nhotel\nhumor\nindex\njapan\nkilo\nlemon\nliter\nlotus\nmango\nmelon\nmenu\nmeter\nmetro\nmineral\nmodel\nmusic\nobject\npiano\npirate\nplastic\nradio\nreport\nsignal\nsport\nstudio\nsubject\nsuper\ntango\ntaxi\ntempo\ntennis\ntextile\ntokyo\ntotal\ntourist\nvideo\nvisa\nacademy\nalfred\natlanta\natomic\nbarbara\nbazaar\nbrother\nbudget\ncabaret\ncadet\ncandle\ncapsule\ncaviar\nchannel\nchapter\ncircle\ncobalt\ncomrade\ncondor\ncrimson\ncyclone\ndarwin\ndeclare\ndenver\ndesert\ndivide\ndolby\ndomain\ndouble\neagle\necho\neclipse\neditor\neducate\nedward\neffect\nelectra\nemerald\nemotion\nempire\neternal\nevening\nexhibit\nexpand\nexplore\nextreme\nferrari\nforget\nfreedom\nfriday\nfuji\ngalileo\ngenesis\ngravity\nhabitat\nhamlet\nharlem\nhelium\nholiday\nhunter\nibiza\niceberg\nimagine\ninfant\nisotope\njackson\njamaica\njasmine\njava\njessica\nkitchen\nlazarus\nletter\nlicense\nlithium\nloyal\nlucky\nmagenta\nmanual\nmarble\nmaxwell\nmayor\nmonarch\nmonday\nmoney\nmorning\nmother\nmystery\nnative\nnectar\nnelson\nnetwork\nnikita\nnobel\nnobody\nnominal\nnorway\nnothing\nnumber\noctober\noffice\noliver\nopinion\noption\norder\noutside\npackage\npandora\npanther\npapa\npattern\npedro\npencil\npeople\nphantom\nphilips\npioneer\npluto\npodium\nportal\npotato\nprocess\nproxy\npupil\npython\nquality\nquarter\nquiet\nrabbit\nradical\nradius\nrainbow\nramirez\nravioli\nraymond\nrespect\nrespond\nresult\nresume\nrichard\nriver\nroger\nroman\nrondo\nsabrina\nsalary\nsalsa\nsample\nsamuel\nsaturn\nsavage\nscarlet\nscorpio\nsector\nserpent\nshampoo\nsharon\nsilence\nsimple\nsociety\nsonar\nsonata\nsoprano\nsparta\nspider\nsponsor\nabraham\naction\nactive\nactor\nadam\naddress\nadmiral\nadrian\nagenda\nagent\nairline\nairport\nalabama\naladdin\nalarm\nalgebra\nalibi\nalice\nalien\nalmond\nalpine\namber\namigo\nammonia\nanalyze\nanatomy\nangel\nannual\nanswer\napple\narchive\narctic\narena\narizona\narmada\narnold\narsenal\narthur\nasia\naspect\nathena\naudio\naugust\naustria\navenue\naverage\naxiom\naztec\nbagel\nbaker\nbalance\nballad\nballet\nbambino\nbamboo\nbaron\nbasic\nbasket\nbattery\nbelgium\nbenefit\nberlin\nbermuda\nbernard\nbicycle\nbinary\nbiology\nbishop\nblitz\nblock\nblonde\nbonjour\nboris\nboston\nbottle\nboxer\nbrandy\nbravo\nbrazil\nbridge\nbritish\nbronze\nbrown\nbruce\nbruno\nbrush\nburger\nburma\ncabinet\ncactus\ncafe\ncairo\ncalypso\ncamel\ncampus\ncanal\ncannon\ncanoe\ncantina\ncanvas\ncanyon\ncapital\ncaramel\ncaravan\ncareer\ncargo\ncarlo\ncarol\ncarpet\ncartel\ncartoon\ncastle\ncastro\ncecilia\ncement\ncenter\ncentury\nceramic\nchamber\nchance\nchange\nchaos\ncharlie\ncharm\ncharter\ncheese\nchef\nchemist\ncherry\nchess\nchicago\nchicken\nchief\nchina\ncigar\ncircus\ncity\nclara\nclassic\nclaudia\nclean\nclient\nclimax\nclinic\nclock\nclub\ncockpit\ncoconut\ncola\ncollect\ncolombo\ncolony\ncolor\ncombat\ncomedy\ncommand\ncompany\nconcert\nconnect\nconsul\ncontact\ncontour\ncontrol\nconvert\ncopy\ncorner\ncorona\ncorrect\ncosmos\ncouple\ncourage\ncowboy\ncraft\ncrash\ncricket\ncrown\ncuba\ndallas\ndance\ndaniel\ndecade\ndecimal\ndegree\ndelete\ndeliver\ndelphi\ndeluxe\ndemand\ndemo\ndenmark\nderby\ndesign\ndetect\ndevelop\ndiagram\ndiamond\ndiana\ndiego\ndiesel\ndiet\ndigital\ndilemma\ndirect\ndisco\ndisney\ndistant\ndollar\ndolphin\ndonald\ndrink\ndriver\ndublin\nduet\ndynamic\nearth\neast\necology\neconomy\nedgar\negypt\nelastic\nelegant\nelement\nelite\nelvis\nemail\nempty\nenergy\nengine\nenglish\nepisode\nequator\nescape\nescort\nethnic\neurope\neverest\nevident\nexact\nexample\nexit\nexotic\nexport\nexpress\nfactor\nfalcon\nfamily\nfantasy\nfashion\nfiber\nfiction\nfidel\nfiesta\nfigure\nfilm\nfilter\nfinance\nfinish\nfinland\nfirst\nflag\nflash\nflorida\nflower\nfluid\nflute\nfolio\nford\nforest\nformal\nformula\nfortune\nforward\nfragile\nfrance\nfrank\nfresh\nfriend\nfrozen\nfuture\ngabriel\ngamma\ngarage\ngarcia\ngarden\ngarlic\ngemini\ngeneral\ngenetic\ngenius\ngermany\ngloria\ngold\ngolf\ngondola\ngong\ngood\ngordon\ngorilla\ngrand\ngranite\ngraph\ngreen\ngroup\nguide\nguitar\nguru\nhand\nhappy\nharbor\nharvard\nhavana\nhawaii\nhelena\nhello\nhenry\nhilton\nhistory\nhorizon\nhouse\nhuman\nicon\nidea\nigloo\nigor\nimage\nimpact\nimport\nindia\nindigo\ninput\ninsect\ninstant\niris\nitalian\njacket\njacob\njaguar\njanet\njargon\njazz\njeep\njohn\njoker\njordan\njudo\njumbo\njune\njungle\njunior\njupiter\nkarate\nkarma\nkayak\nkermit\nking\nkoala\nkorea\nlabor\nlady\nlagoon\nlaptop\nlaser\nlatin\nlava\nlecture\nleft\nlegal\nlevel\nlexicon\nliberal\nlibra\nlily\nlimbo\nlimit\nlinda\nlinear\nlion\nliquid\nlittle\nllama\nlobby\nlobster\nlocal\nlogic\nlogo\nlola\nlondon\nlucas\nlunar\nmachine\nmacro\nmadam\nmadonna\nmadrid\nmaestro\nmagic\nmagnet\nmagnum\nmailbox\nmajor\nmama\nmambo\nmanager\nmanila\nmarco\nmarina\nmarket\nmars\nmartin\nmarvin\nmary\nmaster\nmatrix\nmaximum\nmedia\nmedical\nmega\nmelody\nmemo\nmental\nmentor\nmercury\nmessage\nmetal\nmeteor\nmethod\nmexico\nmiami\nmicro\nmilk\nmillion\nminimum\nminus\nminute\nmiracle\nmirage\nmiranda\nmister\nmixer\nmobile\nmodem\nmodern\nmodular\nmoment\nmonaco\nmonica\nmonitor\nmono\nmonster\nmontana\nmorgan\nmotel\nmotif\nmotor\nmozart\nmulti\nmuseum\nmustang\nnatural\nneon\nnepal\nneptune\nnerve\nneutral\nnevada\nnews\nnext\nninja\nnirvana\nnormal\nnova\nnovel\nnuclear\nnumeric\nnylon\noasis\nobserve\nocean\noctopus\nolivia\nolympic\nomega\nopera\noptic\noptimal\norange\norbit\norganic\norient\norigin\norlando\noscar\noxford\noxygen\nozone\npablo\npacific\npagoda\npalace\npamela\npanama\npancake\npanda\npanel\npanic\nparadox\npardon\nparis\nparker\nparking\nparody\npartner\npassage\npassive\npasta\npastel\npatent\npatient\npatriot\npatrol\npegasus\npelican\npenguin\npepper\npercent\nperfect\nperfume\nperiod\npermit\nperson\nperu\nphone\nphoto\npicasso\npicnic\npicture\npigment\npilgrim\npilot\npixel\npizza\nplanet\nplasma\nplaza\npocket\npoem\npoetic\npoker\npolaris\npolice\npolitic\npolo\npolygon\npony\npopcorn\npopular\npostage\nprecise\nprefix\npremium\npresent\nprice\nprince\nprinter\nprism\nprivate\nprize\nproduct\nprofile\nprogram\nproject\nprotect\nproton\npublic\npulse\npuma\npump\npyramid\nqueen\nradar\nralph\nrandom\nrapid\nrebel\nrecord\nrecycle\nreflex\nreform\nregard\nregular\nrelax\nreptile\nreverse\nricardo\nright\nringo\nrisk\nritual\nrobert\nrobot\nrocket\nrodeo\nromeo\nroyal\nrussian\nsafari\nsalad\nsalami\nsalmon\nsalon\nsalute\nsamba\nsandra\nsantana\nsardine\nschool\nscoop\nscratch\nscreen\nscript\nscroll\nsecond\nsecret\nsection\nsegment\nselect\nseminar\nsenator\nsenior\nsensor\nserial\nservice\nshadow\nsharp\nsheriff\nshock\nshort\nshrink\nsierra\nsilicon\nsilk\nsilver\nsimilar\nsimon\nsingle\nsiren\nslang\nslogan\nsmart\nsmoke\nsnake\nsocial\nsoda\nsolar\nsolid\nsolo\nsonic\nsource\nsoviet\nspecial\nspeed\nsphere\nspiral\nspirit\nspring\nstatic\nstatus\nstereo\nstone\nstop\nstreet\nstrong\nstudent\nstyle\nsultan\nsusan\nsushi\nsuzuki\nswitch\nsymbol\nsystem\ntactic\ntahiti\ntalent\ntarzan\ntelex\ntexas\ntheory\nthermos\ntiger\ntitanic\ntomato\ntopic\ntornado\ntoronto\ntorpedo\ntotem\ntractor\ntraffic\ntransit\ntrapeze\ntravel\ntribal\ntrick\ntrident\ntrilogy\ntripod\ntropic\ntrumpet\ntulip\ntuna\nturbo\ntwist\nultra\nuniform\nunion\nuranium\nvacuum\nvalid\nvampire\nvanilla\nvatican\nvelvet\nventura\nvenus\nvertigo\nveteran\nvictor\nvienna\nviking\nvillage\nvincent\nviolet\nviolin\nvirtual\nvirus\nvision\nvisitor\nvisual\nvitamin\nviva\nvocal\nvodka\nvolcano\nvoltage\nvolume\nvoyage\nwater\nweekend\nwelcome\nwestern\nwindow\nwinter\nwizard\nwolf\nworld\nxray\nyankee\nyoga\nyogurt\nyoyo\nzebra\nzero\nzigzag\nzipper\nzodiac\nzoom\nacid\nadios\nagatha\nalamo\nalert\nalmanac\naloha\nandrea\nanita\narcade\naurora\navalon\nbaby\nbaggage\nballoon\nbank\nbasil\nbegin\nbiscuit\nblue\nbombay\nbotanic\nbrain\nbrenda\nbrigade\ncable\ncalibre\ncarmen\ncello\nceltic\nchariot\nchrome\ncitrus\ncivil\ncloud\ncombine\ncommon\ncool\ncopper\ncoral\ncrater\ncubic\ncupid\ncycle\ndepend\ndoor\ndream\ndynasty\nedison\nedition\nenigma\nequal\neric\nevent\nevita\nexodus\nextend\nfamous\nfarmer\nfood\nfossil\nfrog\nfruit\ngeneva\ngentle\ngeorge\ngiant\ngilbert\ngossip\ngram\ngreek\ngrille\nhammer\nharvest\nhazard\nheaven\nherbert\nheroic\nhexagon\nhusband\nimmune\ninca\ninch\ninitial\nisabel\nivory\njason\njerome\njoel\njoshua\njournal\njudge\njuliet\njump\njustice\nkimono\nkinetic\nleonid\nleopard\nlima\nmaze\nmedusa\nmember\nmemphis\nmichael\nmiguel\nmilan\nmile\nmiller\nmimic\nmimosa\nmission\nmonkey\nmoral\nmoses\nmouse\nnancy\nnatasha\nnebula\nnickel\nnina\nnoise\norchid\noregano\norigami\norinoco\norion\nothello\npaper\npaprika\nprelude\nprepare\npretend\npromise\nprosper\nprovide\npuzzle\nremote\nrepair\nreply\nrival\nriviera\nrobin\nrose\nrover\nrudolf\nsaga\nsahara\nscholar\nshelter\nship\nshoe\nsigma\nsister\nsleep\nsmile\nspain\nspark\nsplit\nspray\nsquare\nstadium\nstar\nstorm\nstory\nstrange\nstretch\nstuart\nsubway\nsugar\nsulfur\nsummer\nsurvive\nsweet\nswim\ntable\ntaboo\ntarget\nteacher\ntelecom\ntemple\ntibet\nticket\ntina\ntoday\ntoga\ntommy\ntower\ntrivial\ntunnel\nturtle\ntwin\nuncle\nunicorn\nunique\nupdate\nvalery\nvega\nversion\nvoodoo\nwarning\nwilliam\nwonder\nyear\nyellow\nyoung\nabsent\nabsorb\nabsurd\naccent\nalfonso\nalias\nambient\nanagram\nandy\nanvil\nappear\napropos\narcher\nariel\narmor\narrow\naustin\navatar\naxis\nbaboon\nbahama\nbali\nbalsa\nbarcode\nbazooka\nbeach\nbeast\nbeatles\nbeauty\nbefore\nbenny\nbetty\nbetween\nbeyond\nbilly\nbison\nblast\nbless\nbogart\nbonanza\nbook\nborder\nbrave\nbread\nbreak\nbroken\nbucket\nbuenos\nbuffalo\nbundle\nbutton\nbuzzer\nbyte\ncaesar\ncamilla\ncanary\ncandid\ncarrot\ncave\nchant\nchild\nchoice\nchris\ncipher\nclarion\nclark\nclever\ncliff\nclone\nconan\nconduct\ncongo\ncostume\ncotton\ncover\ncrack\ncurrent\ndanube\ndata\ndecide\ndeposit\ndesire\ndetail\ndexter\ndinner\ndonor\ndruid\ndrum\neasy\neddie\nenjoy\nenrico\nepoxy\nerosion\nexcept\nexile\nexplain\nfame\nfast\nfather\nfelix\nfield\nfiona\nfire\nfish\nflame\nflex\nflipper\nfloat\nflood\nfloor\nforbid\nforever\nfractal\nframe\nfreddie\nfront\nfuel\ngallop\ngame\ngarbo\ngate\ngelatin\ngibson\nginger\ngiraffe\ngizmo\nglass\ngoblin\ngopher\ngrace\ngray\ngregory\ngrid\ngriffin\nground\nguest\ngustav\ngyro\nhair\nhalt\nharris\nheart\nheavy\nherman\nhippie\nhobby\nhoney\nhope\nhorse\nhostel\nhydro\nimitate\ninfo\ningrid\ninside\ninvent\ninvest\ninvite\nivan\njames\njester\njimmy\njoin\njoseph\njuice\njulius\njuly\nkansas\nkarl\nkevin\nkiwi\nladder\nlake\nlaura\nlearn\nlegacy\nlegend\nlesson\nlife\nlight\nlist\nlocate\nlopez\nlorenzo\nlove\nlunch\nmalta\nmammal\nmargin\nmargo\nmarion\nmask\nmatch\nmayday\nmeaning\nmercy\nmiddle\nmike\nmirror\nmodest\nmorph\nmorris\nmystic\nnadia\nnato\nnavy\nneedle\nneuron\nnever\nnewton\nnice\nnight\nnissan\nnitro\nnixon\nnorth\noberon\noctavia\nohio\nolga\nopen\nopus\norca\noval\nowner\npage\npaint\npalma\nparent\nparlor\nparole\npaul\npeace\npearl\nperform\nphoenix\nphrase\npierre\npinball\nplace\nplate\nplato\nplume\npogo\npoint\npolka\nponcho\npowder\nprague\npress\npresto\npretty\nprime\npromo\nquest\nquick\nquiz\nquota\nrace\nrachel\nraja\nranger\nregion\nremark\nrent\nreward\nrhino\nribbon\nrider\nroad\nrodent\nround\nrubber\nruby\nrufus\nsabine\nsaddle\nsailor\nsaint\nsalt\nscale\nscuba\nseason\nsecure\nshake\nshallow\nshannon\nshave\nshelf\nsherman\nshine\nshirt\nside\nsinatra\nsincere\nsize\nslalom\nslow\nsmall\nsnow\nsofia\nsong\nsound\nsouth\nspeech\nspell\nspend\nspoon\nstage\nstamp\nstand\nstate\nstella\nstick\nsting\nstock\nstore\nsunday\nsunset\nsupport\nsupreme\nsweden\nswing\ntape\ntavern\nthink\nthomas\ntictac\ntime\ntoast\ntobacco\ntonight\ntorch\ntorso\ntouch\ntoyota\ntrade\ntribune\ntrinity\ntriton\ntruck\ntrust\ntype\nunder\nunit\nurban\nurgent\nuser\nvalue\nvendor\nvenice\nverona\nvibrate\nvirgo\nvisible\nvista\nvital\nvoice\nvortex\nwaiter\nwatch\nwave\nweather\nwedding\nwheel\nwhiskey\nwisdom\nandroid\nannex\narmani\ncake\nconfide\ndeal\ndefine\ndispute\ngenuine\nidiom\nimpress\ninclude\nironic\nnull\nnurse\nobscure\nprefer\nprodigy\nego\nfax\njet\njob\nrio\nski\nyes\n"
  },
  {
    "path": "resources/docker-run.sh",
    "content": "#!/bin/sh\n\ndocker run \\\n  -it --rm \\\n  --user \"$(id -u):$(id -g)\" \\\n  --volume $(pwd)/data:/data  \\\n  matrix-registration:latest  \\\n  \"$@\"\n"
  },
  {
    "path": "resources/docker-serve.sh",
    "content": "#!/bin/sh\n\ndocker run \\\n  -d \\\n  --user \"$(id -u):$(id -g)\" \\\n  --network matrix \\\n  --publish 5000:5000/tcp \\\n  --volume $(pwd)/data:/data \\\n  matrix-registration:latest\n"
  },
  {
    "path": "resources/example.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Matrix Registration</title>\n  <style>\n    input:invalid {\n      border: 2px dashed red;\n    }\n\n    input:valid {\n      border: 2px solid black;\n    }\n    input {\n      width: 100%;\n    }\n  </style>\n</head>\n\n<body>\n  <section style=\"display:flex;justify-content:center;align-items:center;\">\n    <!-- make sure to adjust port here -->\n    <form action=\"localhost:5000/register\" method=\"post\">\n      <label for=\"username\"> Enter your username:</label><br>\n      <input id=\"username\" name=\"username\" type=\"text\"\n      required pattern=\"^@?[a-zA-Z_\\-=\\.\\/0-9]+(:matrix\\.org)?$\" \n      required minlength=\"1\" maxlength=\"200\">\n      <!-- change to your homeserver -->\n      <br>\n      <label for=\"password\">Enter your password:</label><br>\n      <input id=\"password\" name=\"password\" type=\"password\"\n      required minlength=\"8\" maxlength=\"128\">\n      <br>\n      <label for=\"confirm_password\">Repeat your password:</label><br>\n      <input id=\"confirm_password\" name=\"confirm\" type=\"password\"\n      required>\n      <br>\n      <label for=\"token\">Enter your invite token:</label><br>\n      <input id=\"token\" name=\"token\" type=\"text\"\n      required pattern=\"^([A-Z][a-z]+)+$\">\n      <br><br>\n      <input type=\"submit\" value=\"register\">\n    </form>\n  </section>\n  <script>\n    // all javascript here is optional, the registration form works fine without\n\n    // see https://stackoverflow.com/a/901144/3779427\n    function getParameterByName(name, url) {\n        if (!url) url = window.location.href;\n        name = name.replace(/[\\[\\]]/g, \"\\\\$&\");\n        var regex = new RegExp(\"[?&]\" + name + \"(=([^&#]*)|&|#|$)\"),\n            results = regex.exec(url);\n        if (!results) return null;\n        if (!results[2]) return '';\n        return decodeURIComponent(results[2].replace(/\\+/g, \" \"));\n    }\n\n    // set token input to \"?token=\" query parameter\n    document.getElementById('token').value = getParameterByName(\"token\");\n\n    // html5 validators\n    var username = document.getElementById(\"username\");\n    var password = document.getElementById(\"password\");\n    var confirm_password = document.getElementById(\"confirm_password\");\n    var token = document.getElementById(\"token\");\n\n    username.addEventListener(\"input\", function (event) {\n      if (username.validity.typeMismatch) {\n        username.setCustomValidity(\"format: @username:matrix.org\");\n      } else {\n        username.setCustomValidity(\"\");\n      }\n    });\n\n    token.addEventListener(\"input\", function (event) {\n      if (token.validity.typeMismatch) {\n        token.setCustomValidity(\"case-sensitive, e.g: SardineImpactReport\");\n      } else {\n        token.setCustomValidity(\"\");\n      }\n    });\n\n    password.addEventListener(\"input\", function (event) {\n      if (password.validity.typeMismatch) {\n        password.setCustomValidity(\"atleast 8 characters long\");\n      } else {\n        password.setCustomValidity(\"\");\n      }\n    });\n\n    function validatePassword(){\n      if(password.value != confirm_password.value) {\n        confirm_password.setCustomValidity(\"passwords don't match\");\n      } else {\n        confirm_password.setCustomValidity(\"\");\n      }\n    }\n\n    password.onchange = validatePassword;\n    confirm_password.onkeyup = validatePassword;\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\nimport codecs\nimport os\nimport re\nimport setuptools\nimport glob\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\n\ndef read(*parts):\n    with codecs.open(os.path.join(here, *parts), 'r') as fp:\n        return fp.read()\n\n\ndef find_version(*file_paths):\n    version_file = read(*file_paths)\n    version_match = re.search(r\"^__version__ = ['\\\"]([^'\\\"]*)['\\\"]\",\n                              version_file, re.M)\n    if version_match:\n        return version_match.group(1)\n    raise RuntimeError(\"Unable to find version string.\")\n\n\ntest_requirements = [\n        \"parameterized>=0.7.0\"\n]\n\n\nsetuptools.setup(\n    name='matrix-registration',\n    version=find_version(\"matrix_registration\", \"__init__.py\"),\n    description='token based matrix registration app',\n    author='Jona Abdinghoff (ZerataX)',\n    author_email='mail@zera.tax',\n    long_description=open(\"README.md\").read(),\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/zeratax/matrix-registration\",\n    packages=['matrix_registration'],\n    package_data={'matrix_registration': ['*.txt','*.json',\n                                          'translations/*.yaml',\n                                          'templates/*.html',\n                                          'static/css/*.css',\n                                          'static/fonts/*.woff2',\n                                          'static/images/*.jpg',\n                                          'static/images/*.png',\n                                          'static/images/*.ico']},\n    python_requires='~=3.7',\n    install_requires=[\n        \"alembic>=1.8\",\n        \"appdirs>=1.4.4\",\n        \"Flask>=2.2\",\n        \"Flask-SQLAlchemy>=2.5.1\",\n        \"flask-cors>=3.0.10\",\n        \"flask-httpauth>=4.7.0\",\n        \"flask-limiter>=2.6\",\n        \"PyYAML>=6.0\",\n        \"jsonschema>=4.17\",\n        \"requests>=2.28\",\n        \"SQLAlchemy>=1.4\",\n        \"waitress>=2.1\",\n        \"WTForms>=3.0\"\n    ],\n    tests_require=test_requirements,\n    extras_require={\n        \"postgres\":  [\"psycopg2-binary>=2.8.4\"],\n        \"testing\": test_requirements\n    },\n    classifiers=[\n        \"Development Status :: 4 - Beta\",\n        \"Topic :: Communications :: Chat\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Programming Language :: Python :: 3.7\",\n        \"Programming Language :: Python :: 3.8\",\n        \"Programming Language :: Python :: 3.9\"\n    ],\n    entry_points={\n        'console_scripts': [\n            'matrix-registration=matrix_registration.app:cli'\n        ],\n    },\n    data_files=[\n        (\"config\", [\"config.sample.yaml\"]),\n        (\".\", [\"alembic.ini\"]),\n        (\"alembic\", [\"alembic/env.py\"]),\n        (\"alembic/versions\", glob.glob(\"alembic/versions/*.py\"))\n    ]\n)\n"
  },
  {
    "path": "shell.nix",
    "content": "{ pkgs ? import <nixpkgs> { } }:\n\n(let matrix-registration = pkgs.callPackage ./default.nix { inherit pkgs; };\nin pkgs.python3.withPackages\n(ps: [ matrix-registration pkgs.black ps.alembic ps.parameterized ps.flake8 ])).env\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/context.py",
    "content": "import os\nimport sys\n\nsys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\")))\n\nimport matrix_registration\n"
  },
  {
    "path": "tests/localhost.log.config",
    "content": "version: 1\nformatters:\n  precise:\n    format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'\nhandlers:\n  console:\n    class: logging.StreamHandler\n    formatter: precise\nloggers:\n    synapse.storage.SQL:\n        level: INFO\nroot:\n    level: INFO\n    handlers: [console]\ndisable_existing_loggers: false\n"
  },
  {
    "path": "tests/localhost.signing.key",
    "content": "ed25519 a_snwG JXtOu5WvTJOETWmItITGUlnqWy6WO4Ovew2flTRYD90\n"
  },
  {
    "path": "tests/test_registration.py",
    "content": "# -*- coding: utf-8 -*-\n# Standard library imports...\nimport hashlib\nimport hmac\nimport json\nimport logging.config\nimport os\nimport random\nimport re\nimport string\nimport sys\nimport unittest\nfrom datetime import datetime\nfrom unittest.mock import patch\nfrom urllib.parse import urlparse\n\n# Third-party imports...\nimport yaml\nfrom parameterized import parameterized\nfrom requests import exceptions\n\n# Local imports...\ntry:\n    from .context import matrix_registration\nexcept ModuleNotFoundError:\n    from context import matrix_registration\nfrom matrix_registration.config import Config\nfrom matrix_registration.tokens import db\nfrom matrix_registration.app import (\n    create_app,\n    cli,\n)\n\nlogger = logging.getLogger(__name__)\n\nLOGGING = {\n    \"version\": 1,\n    \"root\": {\"level\": \"NOTSET\", \"handlers\": [\"console\"]},\n    \"formatters\": {\n        \"precise\": {\"format\": \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"}\n    },\n    \"handlers\": {\n        \"console\": {\n            \"class\": \"logging.StreamHandler\",\n            \"level\": \"NOTSET\",\n            \"formatter\": \"precise\",\n            \"stream\": \"ext://sys.stdout\",\n        }\n    },\n}\n\nGOOD_CONFIG = {\n    \"server_location\": \"https://matrix.org\",\n    \"server_name\": \"matrix.org\",\n    \"registration_shared_secret\": \"coolsharesecret\",\n    \"admin_api_shared_secret\": \"coolpassword\",\n    \"base_url\": \"/element\",\n    \"client_redirect\": \"\",\n    \"client_logo\": \"\",\n    \"db\": \"sqlite:///%s/tests/db.sqlite\" % (os.getcwd(),),\n    \"host\": \"\",\n    \"port\": 5000,\n    \"rate_limit\": [\"1000 per day\", \"100 per minute\"],\n    \"allow_cors\": False,\n    \"password\": {\"min_length\": 8},\n    \"username\": {\n        \"validation_regex\": [\"[a-z\\d]\"],\n        \"invalidation_regex\": [\".*?(admin|support).*?\"],\n    },\n    \"ip_logging\": False,\n    \"logging\": LOGGING,\n}\n\nBAD_CONFIG1 = dict(  # wrong matrix server location -> 500\n    GOOD_CONFIG.items(),\n    server_location=\"https://wronghs.org\",\n)\n\nBAD_CONFIG2 = dict(  # wrong admin secret password -> 401\n    GOOD_CONFIG.items(),\n    admin_api_shared_secret=\"wrongpassword\",\n)\n\nBAD_CONFIG3 = dict(  # wrong matrix shared password -> 500\n    GOOD_CONFIG.items(),\n    registration_shared_secret=\"wrongsecret\",\n)\n\nusernames = []\nnonces = []\nlogging.config.dictConfig(LOGGING)\n\n\ndef mock_new_user(username):\n    access_token = \"\".join(\n        random.choices(string.ascii_lowercase + string.digits, k=256)\n    )\n    device_id = \"\".join(random.choices(string.ascii_uppercase, k=8))\n    home_server = matrix_registration.config.config.server_location\n    username = username.rsplit(\":\")[0].split(\"@\")[-1]\n    user_id = \"@{}:{}\".format(username, home_server)\n    usernames.append(username)\n\n    user = {\n        \"access_token\": access_token,\n        \"device_id\": device_id,\n        \"home_server\": home_server,\n        \"user_id\": user_id,\n    }\n    return user\n\n\ndef mocked__get_nonce(server_location):\n    nonce = \"\".join(random.choices(string.ascii_lowercase + string.digits, k=129))\n    nonces.append(nonce)\n    return nonce\n\n\ndef mocked_requests_post(*args, **kwargs):\n    class MockResponse:\n        def __init__(self, json_data, status_code):\n            self.json_data = json_data\n            self.status_code = status_code\n\n        def json(self):\n            return self.json_data\n\n        def raise_for_status(self):\n            if self.status_code == 200:\n                return self.status_code\n            else:\n                raise exceptions.HTTPError(response=self)\n\n    # print(args[0])\n    # print(matrix_registration.config.config.server_location)\n    domain = urlparse(GOOD_CONFIG[\"server_location\"]).hostname\n    re_mxid = r\"^@?[a-zA-Z_\\-=\\.\\/0-9]+(:\" + re.escape(domain) + r\")?$\"\n    location = \"_synapse/admin/v1/register\"\n\n    if args[0] == \"%s/%s\" % (GOOD_CONFIG[\"server_location\"], location):\n        if kwargs:\n            req = kwargs[\"json\"]\n            if not req[\"nonce\"] in nonces:\n                return MockResponse(\n                    {\"'errcode': 'M_UNKOWN\", \"'error': 'unrecognised nonce'\"}, 400\n                )\n\n            mac = hmac.new(\n                key=str.encode(GOOD_CONFIG[\"registration_shared_secret\"]),\n                digestmod=hashlib.sha1,\n            )\n\n            mac.update(req[\"nonce\"].encode())\n            mac.update(b\"\\x00\")\n            mac.update(req[\"username\"].encode())\n            mac.update(b\"\\x00\")\n            mac.update(req[\"password\"].encode())\n            mac.update(b\"\\x00\")\n            mac.update(b\"admin\" if req[\"admin\"] else b\"notadmin\")\n            mac = mac.hexdigest()\n            if not re.search(re_mxid, req[\"username\"]):\n                return MockResponse(\n                    {\n                        \"'errcode': 'M_INVALID_USERNAME\",\n                        \"'error': 'User ID can only contain\"\n                        + \"characters a-z, 0-9, or '=_-./'\",\n                    },\n                    400,\n                )\n            if req[\"username\"].rsplit(\":\")[0].split(\"@\")[-1] in usernames:\n                return MockResponse(\n                    {\"errcode\": \"M_USER_IN_USE\", \"error\": \"User ID already taken.\"}, 400\n                )\n            if req[\"mac\"] != mac:\n                return MockResponse(\n                    {\"errcode\": \"M_UNKNOWN\", \"error\": \"HMAC incorrect\"}, 403\n                )\n            return MockResponse(mock_new_user(req[\"username\"]), 200)\n    return MockResponse(None, 404)\n\n\nclass TokensTest(unittest.TestCase):\n    def setUp(self):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n        app = create_app(testing=True)\n        with app.app_context():\n            app.config.from_mapping(\n                SQLALCHEMY_DATABASE_URI=matrix_registration.config.config.db,\n                SQLALCHEMY_TRACK_MODIFICATIONS=False,\n            )\n            db.init_app(app)\n            db.create_all()\n\n        self.app = app\n\n    def tearDown(self):\n        os.remove(matrix_registration.config.config.db[10:])\n\n    def test_random_readable_string(self):\n        for n in range(10):\n            string = matrix_registration.tokens.random_readable_string(length=n)\n            words = re.sub(\"([a-z])([A-Z])\", r\"\\1 \\2\", string).split()\n            self.assertEqual(len(words), n)\n\n    def test_tokens_empty(self):\n        with self.app.app_context():\n            test_tokens = matrix_registration.tokens.Tokens()\n\n            # no token should exist at this point\n            self.assertFalse(test_tokens.active(\"\"))\n            test_token = test_tokens.new()\n\n            # no empty token should have been created\n            self.assertFalse(test_tokens.active(\"\"))\n\n    def test_tokens_disable(self):\n        with self.app.app_context():\n            test_tokens = matrix_registration.tokens.Tokens()\n            test_token = test_tokens.new()\n\n            # new tokens should be active first, inactive after disabling it\n            self.assertTrue(test_token.active())\n            self.assertTrue(test_token.disable())\n            self.assertFalse(test_token.active())\n\n            test_token2 = test_tokens.new()\n\n            self.assertTrue(test_tokens.active(test_token2.name))\n            self.assertTrue(test_tokens.disable(test_token2.name))\n            self.assertFalse(test_tokens.active(test_token2.name))\n\n            test_token3 = test_tokens.new()\n            test_token3.use()\n\n            self.assertFalse(test_tokens.active(test_token2.name))\n            self.assertFalse(test_tokens.disable(test_token2.name))\n            self.assertFalse(test_tokens.active(test_token2.name))\n\n    def test_tokens_load(self):\n        with self.app.app_context():\n            test_tokens = matrix_registration.tokens.Tokens()\n\n            test_token = test_tokens.new()\n            test_token2 = test_tokens.new()\n            test_token3 = test_tokens.new(max_usage=True)\n            test_token4 = test_tokens.new(\n                expiration_date=datetime.fromisoformat(\"2111-01-01\")\n            )\n            test_token5 = test_tokens.new(\n                expiration_date=datetime.fromisoformat(\"1999-01-01\")\n            )\n\n            test_tokens.disable(test_token2.name)\n            test_tokens.use(test_token3.name)\n            test_tokens.use(test_token4.name)\n\n            test_tokens.load()\n\n            # token1: active, unused, no expiration date\n            # token2: inactive, unused, no expiration date\n            # token3: used once, one-time, now inactive\n            # token4: active, used once, expiration date\n            # token5: inactive, expiration date\n\n            self.assertEqual(\n                test_token.name, test_tokens.get_token(test_token.name).name\n            )\n            self.assertEqual(\n                test_token2.name, test_tokens.get_token(test_token2.name).name\n            )\n            self.assertEqual(\n                test_token2.active(), test_tokens.get_token(test_token2.name).active()\n            )\n            self.assertEqual(\n                test_token3.used, test_tokens.get_token(test_token3.name).used\n            )\n            self.assertEqual(\n                test_token3.active(), test_tokens.get_token(test_token3.name).active()\n            )\n            self.assertEqual(\n                test_token4.used, test_tokens.get_token(test_token4.name).used\n            )\n            self.assertEqual(\n                test_token4.expiration_date,\n                test_tokens.get_token(test_token4.name).expiration_date,\n            )\n            self.assertEqual(\n                test_token5.active(), test_tokens.get_token(test_token5.name).active()\n            )\n\n    @parameterized.expand(\n        [\n            [None, False],\n            [datetime.fromisoformat(\"2100-01-12\"), False],\n            [None, True],\n            [datetime.fromisoformat(\"2100-01-12\"), True],\n        ]\n    )\n    def test_tokens_new(self, expiration_date, max_usage):\n        with self.app.app_context():\n            test_tokens = matrix_registration.tokens.Tokens()\n            test_token = test_tokens.new(\n                expiration_date=expiration_date, max_usage=max_usage\n            )\n\n            self.assertIsNotNone(test_token)\n            if expiration_date:\n                self.assertIsNotNone(test_token.expiration_date)\n            else:\n                self.assertIsNone(test_token.expiration_date)\n            if max_usage:\n                self.assertTrue(test_token.max_usage)\n            else:\n                self.assertFalse(test_token.max_usage)\n            self.assertTrue(test_tokens.active(test_token.name))\n\n    @parameterized.expand(\n        [\n            [None, False, 10, True],\n            [datetime.fromisoformat(\"2100-01-12\"), False, 10, True],\n            [None, True, 1, False],\n            [None, True, 0, True],\n            [datetime.fromisoformat(\"2100-01-12\"), True, 1, False],\n            [datetime.fromisoformat(\"2100-01-12\"), True, 2, False],\n            [datetime.fromisoformat(\"2100-01-12\"), True, 0, True],\n        ]\n    )\n    def test_tokens_active_form(self, expiration_date, max_usage, times_used, active):\n        with self.app.app_context():\n            test_tokens = matrix_registration.tokens.Tokens()\n            test_token = test_tokens.new(\n                expiration_date=expiration_date, max_usage=max_usage\n            )\n\n            for n in range(times_used):\n                test_tokens.use(test_token.name)\n\n            if not max_usage:\n                self.assertEqual(test_token.used, times_used)\n            elif times_used == 0:\n                self.assertEqual(test_token.used, 0)\n            else:\n                self.assertEqual(test_token.used, 1)\n            self.assertEqual(test_tokens.active(test_token.name), active)\n\n    @parameterized.expand(\n        [\n            [None, True],\n            [datetime.fromisoformat(\"2100-01-12\"), False],\n            [datetime.fromisoformat(\"2200-01-13\"), True],\n        ]\n    )\n    def test_tokens_active(self, expiration_date, active):\n        with self.app.app_context():\n            test_tokens = matrix_registration.tokens.Tokens()\n            test_token = test_tokens.new(expiration_date=expiration_date)\n\n            self.assertEqual(test_tokens.active(test_token.name), True)\n            # date changed to after expiration date\n            with patch(\"matrix_registration.tokens.datetime\") as mock_date:\n                mock_date.now.return_value = datetime.fromisoformat(\"2200-01-12\")\n                self.assertEqual(test_tokens.active(test_token.name), active)\n\n    @parameterized.expand(\n        [\n            [\"DoubleWizardSky\"],\n            [\"null\"],\n            [\"false\"],\n        ]\n    )\n    def test_tokens_repr(self, name):\n        with self.app.app_context():\n            test_token1 = matrix_registration.tokens.Token(name=name)\n\n            self.assertEqual(str(test_token1), name)\n\n    def test_token_repr(self):\n        with self.app.app_context():\n            test_tokens = matrix_registration.tokens.Tokens()\n            test_token1 = test_tokens.new()\n            test_token2 = test_tokens.new()\n            test_token3 = test_tokens.new()\n            test_token4 = test_tokens.new()\n            test_token5 = test_tokens.new()\n\n            expected_answer = (\n                    \"%s, \" % test_token1.name\n                    + \"%s, \" % test_token2.name\n                    + \"%s, \" % test_token3.name\n                    + \"%s, \" % test_token4.name\n                    + \"%s\" % test_token5.name\n            )\n\n            self.assertEqual(str(test_tokens), expected_answer)\n\n\nclass ApiTest(unittest.TestCase):\n    def setUp(self):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n        app = create_app(testing=True)\n        with app.app_context():\n            app.config.from_mapping(\n                SQLALCHEMY_DATABASE_URI=matrix_registration.config.config.db,\n                SQLALCHEMY_TRACK_MODIFICATIONS=False,\n            )\n            db.init_app(app)\n            db.create_all()\n            self.client = app.test_client()\n        self.app = app\n\n    def tearDown(self):\n        os.remove(matrix_registration.config.config.db[10:])\n\n    @parameterized.expand(\n        [\n            [\"test1\", \"test1234\", \"test1234\", True, 200],\n            [\"\", \"test1234\", \"test1234\", True, 400],\n            [\"test2\", \"\", \"test1234\", True, 400],\n            [\"test3\", \"test1234\", \"\", True, 400],\n            [\"test4\", \"test1234\", \"test1234\", False, 400],\n            [\"@test5:matrix.org\", \"test1234\", \"test1234\", True, 200],\n            [\"@test6:wronghs.org\", \"test1234\", \"test1234\", True, 400],\n            [\"test7\", \"test1234\", \"tet1234\", True, 400],\n            [\"teüst8\", \"test1234\", \"test1234\", True, 400],\n            [\"@test9@matrix.org\", \"test1234\", \"test1234\", True, 400],\n            [\"test11@matrix.org\", \"test1234\", \"test1234\", True, 400],\n            [\"\", \"test1234\", \"test1234\", True, 400],\n            [\n                \"\".join(random.choices(string.ascii_uppercase, k=256)),\n                \"test1234\",\n                \"test1234\",\n                True,\n                400,\n            ],\n            [\"@admin:matrix.org\", \"test1234\", \"test1234\", True, 400],\n            [\"matrixadmin123\", \"test1234\", \"test1234\", True, 400],\n        ]\n    )\n    # check form activeators\n    @patch(\"matrix_registration.matrix_api._get_nonce\", side_effect=mocked__get_nonce)\n    @patch(\n        \"matrix_registration.matrix_api.requests.post\", side_effect=mocked_requests_post\n    )\n    def test_register(\n            self, username, password, confirm, token, status, mock_get, mock_nonce\n    ):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(\n                expiration_date=None, max_usage=True\n            )\n            # replace matrix with in config set hs\n            domain = urlparse(\n                matrix_registration.config.config.server_location\n            ).hostname\n            if username:\n                username = username.replace(\"matrix.org\", domain)\n\n            if not token:\n                test_token.name = \"\"\n            rv = self.client.post(\n                \"/register\",\n                data=dict(\n                    username=username,\n                    password=password,\n                    confirm=confirm,\n                    token=test_token.name,\n                ),\n            )\n            if rv.status_code == 200:\n                account_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n                # print(account_data)\n            self.assertEqual(rv.status_code, status)\n\n    @patch(\"matrix_registration.matrix_api._get_nonce\", side_effect=mocked__get_nonce)\n    @patch(\n        \"matrix_registration.matrix_api.requests.post\", side_effect=mocked_requests_post\n    )\n    def test_register_wrong_hs(self, mock_get, mock_nonce):\n        matrix_registration.config.config = Config(data=BAD_CONFIG1)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(\n                expiration_date=None, max_usage=True\n            )\n            rv = self.client.post(\n                \"/register\",\n                data=dict(\n                    username=\"username\",\n                    password=\"password\",\n                    confirm=\"password\",\n                    token=test_token.name,\n                ),\n            )\n            self.assertEqual(rv.status_code, 500)\n\n    @patch(\"matrix_registration.matrix_api._get_nonce\", side_effect=mocked__get_nonce)\n    @patch(\n        \"matrix_registration.matrix_api.requests.post\", side_effect=mocked_requests_post\n    )\n    def test_register_wrong_secret(self, mock_get, mock_nonce):\n        matrix_registration.config.config = Config(data=BAD_CONFIG3)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(\n                expiration_date=None, max_usage=True\n            )\n            rv = self.client.post(\n                \"/register\",\n                data=dict(\n                    username=\"username\",\n                    password=\"password\",\n                    confirm=\"password\",\n                    token=test_token.name,\n                ),\n            )\n            self.assertEqual(rv.status_code, 500)\n\n    def test_get_tokens(self):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(\n                expiration_date=None, max_usage=True\n            )\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.get(\"/api/token\", headers=headers)\n\n            self.assertEqual(rv.status_code, 200)\n            token_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n\n            self.assertEqual(token_data[0][\"expiration_date\"], None)\n            self.assertEqual(token_data[0][\"max_usage\"], True)\n\n    def test_error_get_tokens(self):\n        matrix_registration.config.config = Config(data=BAD_CONFIG2)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(\n                expiration_date=None, max_usage=True\n            )\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            matrix_registration.config.config = Config(data=GOOD_CONFIG)\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.get(\"/api/token\", headers=headers)\n\n            self.assertEqual(rv.status_code, 401)\n            token_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n\n            self.assertEqual(token_data[\"errcode\"], \"MR_BAD_SECRET\")\n            self.assertEqual(token_data[\"error\"], \"wrong shared secret\")\n\n    @parameterized.expand(\n        [\n            [None, True, None],\n            [\"2020-12-24\", False, \"2020-12-24 00:00:00\"],\n            [\"2200-05-12\", True, \"2200-05-12 00:00:00\"],\n        ]\n    )\n    def test_post_token(self, expiration_date, max_usage, parsed_date):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(\n                expiration_date=None, max_usage=True\n            )\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.post(\n                \"/api/token\",\n                data=json.dumps(\n                    dict(expiration_date=expiration_date, max_usage=max_usage)\n                ),\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 200)\n            token_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n            self.assertEqual(token_data[\"expiration_date\"], parsed_date)\n            self.assertEqual(token_data[\"max_usage\"], max_usage)\n            self.assertTrue(token_data[\"name\"] is not None)\n\n    def test_error_post_token(self):\n        matrix_registration.config.config = Config(data=BAD_CONFIG2)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(\n                expiration_date=None, max_usage=True\n            )\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            matrix_registration.config.config = Config(data=GOOD_CONFIG)\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.post(\n                \"/api/token\",\n                data=json.dumps(dict(expiration_date=\"24.12.2020\", max_usage=False)),\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 401)\n            token_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n\n            self.assertEqual(token_data[\"errcode\"], \"MR_BAD_SECRET\")\n            self.assertEqual(token_data[\"error\"], \"wrong shared secret\")\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.post(\n                \"/api/token\",\n                data=json.dumps(dict(expiration_date=\"2020-24-12\", max_usage=False)),\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 400)\n            token_data = json.loads(rv.data.decode(\"utf8\"))\n            self.assertEqual(token_data[\"errcode\"], \"MR_BAD_DATE_FORMAT\")\n            self.assertEqual(token_data[\"error\"], \"date wasn't in YYYY-MM-DD format\")\n\n    def test_patch_token(self):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(max_usage=True)\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.patch(\n                \"/api/token/\" + test_token.name,\n                data=json.dumps(dict(disabled=True)),\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 200)\n            token_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n            self.assertEqual(token_data[\"active\"], False)\n            self.assertEqual(token_data[\"max_usage\"], True)\n            self.assertEqual(token_data[\"name\"], test_token.name)\n\n    def test_error_patch_token(self):\n        matrix_registration.config.config = Config(data=BAD_CONFIG2)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(max_usage=True)\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            matrix_registration.config.config = Config(data=GOOD_CONFIG)\n            rv = self.client.patch(\n                \"/api/token/\" + test_token.name,\n                data=json.dumps(dict(disabled=True)),\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 401)\n            token_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n            self.assertEqual(token_data[\"errcode\"], \"MR_BAD_SECRET\")\n            self.assertEqual(token_data[\"error\"], \"wrong shared secret\")\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.patch(\n                \"/api/token/\" + test_token.name,\n                data=json.dumps(dict(active=False)),\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 400)\n            token_data = json.loads(rv.data.decode(\"utf8\"))\n            self.assertEqual(token_data[\"errcode\"], \"MR_BAD_USER_REQUEST\")\n            self.assertEqual(\n                token_data[\"error\"], \"you're not allowed to change this property\"\n            )\n\n            rv = self.client.patch(\n                \"/api/token/\" + \"nicememe\",\n                data=json.dumps(dict(disabled=True)),\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 404)\n            token_data = json.loads(rv.data.decode(\"utf8\"))\n            self.assertEqual(token_data[\"errcode\"], \"MR_TOKEN_NOT_FOUND\")\n            self.assertEqual(token_data[\"error\"], \"token does not exist\")\n\n    def test_delete_token(self):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(max_usage=True)\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.get(\n                \"/api/token/\" + test_token.name,\n                content_type=\"application/json\",\n                headers=headers,\n            )\n            self.assertEqual(rv.status_code, 200)\n\n            rv = self.client.delete(\n                \"/api/token/\" + test_token.name,\n                content_type=\"application/json\",\n                headers=headers,\n            )\n            self.assertEqual(rv.status_code, 200)\n\n            rv = self.client.get(\n                \"/api/token/\" + test_token.name,\n                content_type=\"application/json\",\n                headers=headers,\n            )\n            self.assertEqual(rv.status_code, 404)\n\n    def test_error_delete_token(self):\n        matrix_registration.config.config = Config(data=BAD_CONFIG2)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(max_usage=True)\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            matrix_registration.config.config = Config(data=GOOD_CONFIG)\n            rv = self.client.delete(\n                \"/api/token/\" + test_token.name,\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 401)\n            token_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n            self.assertEqual(token_data[\"errcode\"], \"MR_BAD_SECRET\")\n            self.assertEqual(token_data[\"error\"], \"wrong shared secret\")\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.delete(\n                \"/api/token/\" + \"nicememe\",\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 404)\n            token_data = json.loads(rv.data.decode(\"utf8\"))\n            self.assertEqual(token_data[\"errcode\"], \"MR_TOKEN_NOT_FOUND\")\n            self.assertEqual(token_data[\"error\"], \"token does not exist\")\n\n    @parameterized.expand(\n        [\n            [None, True, None],\n            [datetime.fromisoformat(\"2020-12-24\"), False, \"2020-12-24 00:00:00\"],\n            [datetime.fromisoformat(\"2200-05-12\"), True, \"2200-05-12 00:00:00\"],\n        ]\n    )\n    def test_get_token(self, expiration_date, max_usage, parsed_date):\n        matrix_registration.config.config = Config(data=BAD_CONFIG2)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(\n                expiration_date=expiration_date, max_usage=max_usage\n            )\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.get(\n                \"/api/token/\" + test_token.name,\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 200)\n            token_data = json.loads(rv.data.decode(\"utf8\"))\n            self.assertEqual(token_data[\"expiration_date\"], parsed_date)\n            self.assertEqual(token_data[\"max_usage\"], max_usage)\n\n    def test_error_get_token(self):\n        matrix_registration.config.config = Config(data=BAD_CONFIG2)\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            test_token = matrix_registration.tokens.tokens.new(max_usage=True)\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            rv = self.client.get(\n                \"/api/token/\" + \"nice_meme\",\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 404)\n            token_data = json.loads(rv.data.decode(\"utf8\"))\n            self.assertEqual(token_data[\"errcode\"], \"MR_TOKEN_NOT_FOUND\")\n            self.assertEqual(token_data[\"error\"], \"token does not exist\")\n\n            matrix_registration.config.config = Config(data=BAD_CONFIG2)\n\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n            matrix_registration.config.config = Config(data=GOOD_CONFIG)\n            rv = self.client.patch(\n                \"/api/token/\" + test_token.name,\n                data=json.dumps(dict(disabled=True)),\n                content_type=\"application/json\",\n                headers=headers,\n            )\n\n            self.assertEqual(rv.status_code, 401)\n            token_data = json.loads(rv.data.decode(\"utf8\").replace(\"'\", '\"'))\n            self.assertEqual(token_data[\"errcode\"], \"MR_BAD_SECRET\")\n            self.assertEqual(token_data[\"error\"], \"wrong shared secret\")\n\n    def test_rate_limit_exempt(self):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n\n        with self.app.app_context():\n            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()\n            secret = matrix_registration.config.config.admin_api_shared_secret\n            headers = {\"Authorization\": \"SharedSecret %s\" % secret}\n\n            for i in range(110):\n                self.client.get(\"/api/token\", headers=headers)\n\n            rv = self.client.get(\"/api/token\", headers=headers)\n            self.assertEqual(rv.status_code, 429)\n\n            for i in range(110):\n                self.client.get(\"/health\")\n\n            rv = self.client.get(\"/health\")\n            self.assertEqual(rv.status_code, 200)\n\n\nclass ConfigTest(unittest.TestCase):\n    def test_config_update(self):\n        matrix_registration.config.config = Config(data=GOOD_CONFIG)\n        self.assertEqual(matrix_registration.config.config.port, GOOD_CONFIG[\"port\"])\n        self.assertEqual(\n            matrix_registration.config.config.server_location,\n            GOOD_CONFIG[\"server_location\"],\n        )\n\n        matrix_registration.config.config.update(BAD_CONFIG1)\n        self.assertEqual(matrix_registration.config.config.port, BAD_CONFIG1[\"port\"])\n        self.assertEqual(\n            matrix_registration.config.config.server_location,\n            BAD_CONFIG1[\"server_location\"],\n        )\n\n    def test_config_path(self):\n        # BAD_CONFIG1_path = \"x\"\n        good_config_path = \"tests/test_config.yaml\"\n\n        with open(good_config_path, \"w\") as outfile:\n            yaml.dump(GOOD_CONFIG, outfile, default_flow_style=False)\n\n        matrix_registration.config.config = Config(path=good_config_path)\n        self.assertIsNotNone(matrix_registration.config.config)\n        os.remove(good_config_path)\n\n\nclass CliTest(unittest.TestCase):\n    path = \"tests/test_config.yaml\"\n    db = \"tests/db.sqlite\"\n\n    def setUp(self):\n        try:\n            os.remove(self.db)\n        except FileNotFoundError:\n            pass\n        with open(self.path, \"w\") as outfile:\n            yaml.dump(GOOD_CONFIG, outfile, default_flow_style=False)\n\n    def tearDown(self):\n        os.remove(self.path)\n        os.remove(self.db)\n\n    def test_create_token(self):\n        runner = create_app().test_cli_runner()\n        generate = runner.invoke(cli, [\"--config-path\", self.path, \"generate\", \"-m\", 1])\n        name1 = generate.output.strip()\n\n        status = runner.invoke(cli, [\"--config-path\", self.path, \"status\", \"-s\", name1])\n        valid, info_dict_string = status.output.strip().split(\"\\n\", 1)\n        self.assertEqual(valid, \"This token is valid\")\n        comparison_dict = {\n            \"name\": name1,\n            \"used\": 0,\n            \"expiration_date\": None,\n            \"max_usage\": 1,\n            \"disabled\": False,\n            \"ips\": [],\n            \"active\": True,\n        }\n        self.assertEqual(json.loads(info_dict_string), comparison_dict)\n\n        runner.invoke(cli, [\"--config-path\", self.path, \"status\", \"-d\", name1])\n        status = runner.invoke(cli, [\"--config-path\", self.path, \"status\", \"-s\", name1])\n        valid, info_dict_string = status.output.strip().split(\"\\n\", 1)\n        self.assertEqual(valid, \"This token is not valid\")\n        comparison_dict = {\n            \"name\": name1,\n            \"used\": 0,\n            \"expiration_date\": None,\n            \"max_usage\": 1,\n            \"disabled\": True,\n            \"ips\": [],\n            \"active\": False,\n        }\n        self.assertEqual(json.loads(info_dict_string), comparison_dict)\n\n        generate = runner.invoke(\n            cli, [\"--config-path\", self.path, \"generate\", \"-e\", \"2220-05-12\"]\n        )\n        name2 = generate.output.strip()\n\n        status = runner.invoke(cli, [\"--config-path\", self.path, \"status\", \"-s\", name2])\n        valid, info_dict_string = status.output.strip().split(\"\\n\", 1)\n        self.assertEqual(valid, \"This token is valid\")\n        comparison_dict = {\n            \"name\": name2,\n            \"used\": 0,\n            \"expiration_date\": \"2220-05-12 00:00:00\",\n            \"max_usage\": 0,\n            \"disabled\": False,\n            \"ips\": [],\n            \"active\": True,\n        }\n        self.assertEqual(json.loads(info_dict_string), comparison_dict)\n\n        status = runner.invoke(cli, [\"--config-path\", self.path, \"status\", \"-l\"])\n        list = status.output.strip()\n        self.assertEqual(list, f\"{name1}, {name2}\")\n\n\nif \"logging\" in sys.argv:\n    logging.basicConfig(level=logging.DEBUG)\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = py37,py38,p39\n[testenv]\ndeps = coveralls\ncommands = coverage erase\n       {envbindir}/python setup.py develop\n       coverage run -p setup.py test\n       coverage combine\n       - coverage html\n"
  }
]