Full Code of ZerataX/matrix-registration for AI

master db7aab3dbfb5 cached
54 files
151.0 KB
38.6k tokens
108 symbols
1 requests
Download .txt
Repository: ZerataX/matrix-registration
Branch: master
Commit: db7aab3dbfb5
Files: 54
Total size: 151.0 KB

Directory structure:
gitextract_w5avl08v/

├── .coveragerc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── docker.yml
│       ├── pypi.yml
│       └── tests.yml
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── alembic/
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
│       ├── 130b5c2275d8_update_ip_token_association.py
│       └── 140a25d5f185_create_tokens_table.py
├── alembic.ini
├── config.sample.yaml
├── default.nix
├── docker.nix
├── matrix_registration/
│   ├── __init__.py
│   ├── api.py
│   ├── app.py
│   ├── config.py
│   ├── config.schema.json
│   ├── constants.py
│   ├── limiter.py
│   ├── matrix_api.py
│   ├── static/
│   │   ├── css/
│   │   │   └── style.css
│   │   └── fonts/
│   │       └── NUNITO-LICENSE
│   ├── templates/
│   │   └── register.html
│   ├── tokens.py
│   ├── translation.py
│   ├── translations/
│   │   ├── messages.de.yaml
│   │   ├── messages.en.yaml
│   │   ├── messages.pt_BR.yaml
│   │   ├── messages.sv.yaml
│   │   └── messages.zh_Hans.yaml
│   └── wordlist.txt
├── resources/
│   ├── docker-run.sh
│   ├── docker-serve.sh
│   └── example.html
├── setup.py
├── shell.nix
├── tests/
│   ├── __init__.py
│   ├── context.py
│   ├── localhost.log.config
│   ├── localhost.signing.key
│   └── test_registration.py
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
source = matrix_registration


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: "🐛 Bug report"
description: Report errors or unexpected behavior
labels: 
- bug
body:
- type: markdown
  attributes:
    value: Please make sure to [search for existing issues](https://github.com/zeratax/matrix-registration/issues) before filing a new one!

- type: dropdown
  attributes:
    label: How did you install matrix-registration?
    multiple: false
    options:
      - pip
      - direct clone from repo
      - docker
      - matrix-docker-ansible-deploy
  validations:
    required: true
    
- type: input
  attributes:
    label: What python version are you running?
    placeholder: "3.7"
    description: Only provide this, when you aren't using docker.
  validations:
    required: false

- type: input
  attributes:
    label: What version of matrix-registration are you running?
    placeholder: "0.9.7"
    description: It's fine to write "latest" if you updated recently or "unknown" if your unsure.
  validations:
    required: true

- type: textarea
  attributes:
    label: Your config.yml
    description: This is not always required - if your are unsure, please provide it.
    placeholder: DO NOT POST PASSWORDS!
  validations:
    required: false

- type: textarea
  attributes:
    label: Your error log
    description: Provide it here if you got one.
    placeholder: |
        matrix[187]: Traceback (most recent call last):
        matrix[187]:   File "/usr/local/lib/python3.8/site-packages/matrix_registration/app.py", line 9, in <module>
        matrix[187]:     from flask_limiter.util import get_ipaddr
        matrix[187]: ImportError: cannot import name 'get_ipaddr' from 'flask_limiter.util'
        systemd[1]: matrix-registration.service: Main process exited, code=exited, status=1/FAILURE
    render: text
  validations:
    required: false
      
- type: dropdown
  attributes:
    label: Area of your issue?
    multiple: false
    options:
      - installation
      - api
      - general usage
      - other
  validations:
    required: true  
    
- type: textarea
  attributes:
    label: What happened
    description: Describe your issue here
  validations:
    required: true  
    
- type: textarea
  attributes:
    label: Steps to reproduce
    placeholder: Tell us the steps required to trigger your bug.
  validations:
    required: false
    


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
  - name: "\U0001F4DA Explanation to all config entries"
    url: https://github.com/zeratax/matrix-registration/wiki#configuration
    about: Need help with your config.yaml?
  - name: "\U0001F4DA Examples for api usage"
    url: https://github.com/zeratax/matrix-registration/wiki/api
    about: Need help with using the api?
  - name: "\U0001F310 Our comunity matrix channel"
    url: https://matrix.to/#/#matrix-registration:dmnd.sh
    about: Need general help?

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: "⭐ Feature / enhancement request"
description: Suggest an idea for this project
labels: 
- enhancement
body:
- type: textarea
  attributes: 
    label: Description of the new feature / enhancement
    placeholder: |
      What is the expected behavior of the proposed feature?
  validations:
    required: true
- type: textarea
  attributes: 
    label: Scenario when this would be used? 
    placeholder: |
      What is the scenario this would be used?
      Is this enhancing to your workflow?
      Are there benefits for the end-user?
  validations:
    required: true
- type: textarea
  attributes: 
    label: Further information
    placeholder: |
      Do you want to provide anything else? Screenshots? Additional context?
  validations:
    required: false


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ master ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ master ]
  schedule:
    - cron: '24 3 * * 3'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'python' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
        # Learn more:
        # 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

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v1
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v1

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v1


================================================
FILE: .github/workflows/docker.yml
================================================
name: Publish Docker Image
on: [ push, pull_request ]

jobs:
  build_image:
    name: Build Docker Image
    runs-on: ubuntu-latest

    outputs:
      tag: ${{ steps.vars.outputs.tag }}

    strategy:
      matrix:
        arch_name: [ 'x64', 'arm64', 'arm32' ]
        include:
        - arch: 'import <nixpkgs> {}'
          arch_name: 'x64'
        - arch: 'import <nixpkgs> { crossSystem.config = "aarch64-unknown-linux-musl"; }'
          arch_name: 'arm64'
        - arch: 'import <nixpkgs> { crossSystem.config = "armv7l-unknown-linux-musl"; }'
          arch_name: 'arm32'
    steps:
      - name: Check out the repo
        uses: actions/checkout@v2
      - name: Get the version
        id: vars
        run: |
          tag="${GITHUB_REF:10}"
          if [[ "${tag}" == v* ]]; then
            echo ::set-output name=tag::$(echo "${tag}")
          else
            echo ::set-output name=tag::latest
          fi

      - name: Install nix
        uses: cachix/install-nix-action@v12
        with:
          nix_path: nixpkgs=channel:nixos-22.11
      - name: Build docker image
        env:
          tag: ${{ steps.vars.outputs.tag }}
        run: nix-build docker.nix --arg pkgs '${{ matrix.arch }}' --argstr tag "${tag}-${arch_name}"

      - name: Rename archive 
        run: mv result image_${{ matrix.arch_name }}.tar.gz
      - uses: actions/upload-artifact@master
        with:
          name: image_${{ matrix.arch_name }}
          path: image_${{ matrix.arch_name }}.tar.gz

  test_image:
    name: Test Docker Image
    runs-on: ubuntu-latest
    needs: build_image

    services:
      postgres:
        image: postgres
        env:
          POSTGRES_PASSWORD: postgres
        options:
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Check out the repo
        uses: actions/checkout@v2
      - uses: actions/download-artifact@master
        with:
          name: image_x64

      - name: Run synapse
        run: |
          docker run -d \
            -e UID=$(id -u) \
            -e GID=$(id -g) \
            --volume="$(pwd)/tests:/data" \
            --network="${{ job.services.postgres.network }}" \
            --name="synapse" \
            matrixdotorg/synapse:latest

      - name: Load docker image
        run: docker load < image_x64.tar.gz
      - name: Run docker image
        id: token
        env:
          tag: ${{ needs.build_image.outputs.tag }}
        run: |
          echo "test alembic"
          docker run --rm \
            --volume="$(pwd)/tests:/data" \
            --network="${{ job.services.postgres.network }}" \
            matrix-registration:$tag \
            alembic upgrade head
          echo "create a token"
          docker run --rm \
            --volume="$(pwd)/tests:/data" \
            --network="${{ job.services.postgres.network }}" \
            matrix-registration:$tag \
            matrix-registration generate 1> token
          echo "serve webpage"
          docker run -d \
            --volume="$(pwd)/tests:/data" \
            --network="${{ job.services.postgres.network }}" \
            --publish 5000:5000 \
            --name="matrix-registration" \
            matrix-registration:$tag

          echo ::set-output name=token::$(cat token)

      - name: Register test account
        run: |
          echo "waiting until matrix-registration is up..."
          for run in {1..5}; do
            healthy=$(docker inspect -f "{{.State.Health.Status}}" matrix-registration)
            echo $healthy
            if [ "$healthy" = "healthy" ]; then
              echo "matrix-registration is up!"
              break
            else
              sleep 1
            fi
          done
          echo "waiting until synapse is up..."
          for run in {1..60}; do
            healthy=$(docker inspect -f "{{.State.Health.Status}}" synapse)
            echo $healthy
            if [ "$healthy" = "healthy" ]; then
              echo "synapse is up!"
              break
            else
              sleep 1
            fi
          done
          # register account
          curl -fSs \
            -F 'username=test' \
            -F 'password=verysecure' \
            -F 'confirm=verysecure' \
            -F 'token=${{ steps.token.outputs.token }}' \
            http://matrix-registration:5000/register
 
      - name: Registering failed, check logs
        if: ${{ failure() }}
        run: |
          cat tests/mreg.log
          docker logs synapse

      - name: Stop manually started containers
        if: ${{ always() }}
        run: docker kill $(docker ps -q)

  push_to_registries:
    name: Push Docker Image to Multiple Registries
    runs-on: ubuntu-latest
    needs: test_image
    if: startsWith(github.ref, 'refs/tags/v')

    strategy:
      matrix:
        hub: [Github, Docker]
        include:
          - hub: Github
            registry: ghcr.io
            username: DOCKER_USERNAME
            password: GITHUB_TOKEN
            image: "/matrix-registration-image"
          - hub: Docker
            registry: docker.io
            username: DOCKER_USERNAME
            password: DOCKER_PASSWORD
            image: ""

    steps:
      - uses: actions/download-artifact@master
        with:
          name: image

      # ${GITHUB_REPOSITORY,,} => zeratax/matrix-registration
      - name: Push to ${{ matrix.hub }} 
        run: |
          echo "login to registry..."
          docker login --username="${{secrets[matrix.username]}}" --password="${{secrets[matrix.password]}}" ${{ matrix.registry }}

          echo "logged in!"

          arches=(x64 amd64 amd32)
          image_names=()

          echo "uploading images for individual arches..."
          for arch in $arches
          do
            image="image_${arch}.tar.gz"

            echo "check if image '${image}' for arch ${arch} exists..."
            if [ ! -f "${image}" ]; then
              break
            fi

            echo "image exists!"

            full_tag="${tag}-${arch}"
            name="${GITHUB_REPOSITORY,,}${{matrix.image}}"

            echo "upload image for arch ${arch}..."
            docker load -i $image
            docker push ${name}:${full_tag}

            image_names=+("${name}:${full_tag}")
            echo "so far uploaded images: '${#image_names[@]}'"
          done

          if [ ${#image_names[@]} -eq 0 ]; then
            echo "no images uploaded!"
            return 1
          fi

          echo "creating a manifest to associate arch images with multi-arch image..."
          docker manifest create --amend ${name}:${tag} ${image_names[@]}
          echo "uploading manifest..."
          docker manifest push ${name}:${tag}
          echo "success!"
        env:
          tag: ${{ needs.build_image.outputs.tag }}


================================================
FILE: .github/workflows/pypi.yml
================================================
name: Publish Python Package

on:
  push:
    # Sequence of patterns matched against refs/tags
    tags:
      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

jobs:
  build-and-publish:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@master
    - name: Set up Python 3.9
      uses: actions/setup-python@v1
      with:
        python-version: 3.9
    - name: Install pypa/build
      run: >-
        python -m
        pip install
        build
        --user
    - name: Build a binary wheel 👷
      run: >-
        python -m
        build
        --wheel
        --outdir dist/
        .
    - name: Publish Python 🐍 distributions 📦 to PyPI
      uses: pypa/gh-action-pypi-publish@master
      with:
        password: ${{ secrets.PYPI_API_TOKEN }}


================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests

on:
  push:
    branches:
    - master
  pull_request:
    branches:
    - master

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: [ '3.7', '3.8', '3.9' ]
    name: Python ${{ matrix.python-version }}
    steps:
      - uses: actions/checkout@v2
      - name: Setup python
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python setup.py develop
      - name: Lint with flake8 👕
        run: |
          pip install flake8
          # stop the build if there are Python syntax errors or undefined names
          flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
          # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
          flake8 matrix_registration --per-file-ignores="__init__.py:F401" --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
      - name: Test with parameterized unit tests 🚨
        run: |
          python setup.py test


================================================
FILE: .gitignore
================================================
# Project specific
*.yaml
*.conf
!config.sample.yaml
!tests/test_config.yaml
result
data/

!matrix_registration/translations/*.yaml
# vscode
.vscode/
# nose
man/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
share/
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
#  Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json


================================================
FILE: .travis.yml
================================================
language: python
matrix:
  include:
    - python: 3.7
      dist: focal
      sudo: true
    - python: 3.8
      dist: focal
      sudo: true
    - python: 3.9
      dist: focal
      sudo: true
install: 
  - pip install tox-travis
  - pip install coveralls
script: tox

after_success:
    coveralls


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
 advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
 address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
 professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at mail@zera.tax. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq


================================================
FILE: CONTRIBUTING.md
================================================
# CONTRIBUTING
## Code of Conduct
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
## How Can I Contribute?
### Issues
Filling issues is a great and easy way to help find bugs and get new features implemented.
#### Bugs
If you're reporting a security issue, please email me at security@zera.tax, otherwise
if you're reporting a bug, please fill out this [form](https://github.com/ZerataX/matrix-registration/issues/new?labels=bug&template=bug_report.md).
#### Feature Requests
If 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).
#### Translations
You can translate the registration page over at https://l10n.dmnd.sh/engage/matrix-registration/

[![Translation status](https://l10n.dmnd.sh/widgets/matrix-registration/-/open-graph.png)](https://l10n.dmnd.sh/engage/matrix-registration/)

#### Getting Started

To 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.


If you have any further questions about how to contribute, please make an issue on the GitHub page.


### Pull Requests

Every PR should not break tests and ideally include tests for added functionality.
Also it is recommend to follow the [PEP8](https://www.python.org/dev/peps/pep-0008/) styleguide
#### Setting up the Project

```bash
git clone https://github.com/ZerataX/matrix-registration.git
cd matrix-registration

virtualenv -p /usr/bin/python3.6 .
source ./bin/activate

python setup.py develop
cp config.sample.yaml config.yaml
```

and edit config.yaml

You can run tests by executing the following command in the project root
```bash
python setup.py test
```




================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Jona Abdinghoff

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<img src="resources/logo.png" width="300">

[![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)

# matrix-registration

A simple Python application enabling token-based registration for matrix servers.

You 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.

With matrix-registration, you can quickly generate tokens on the fly and share them with your friends to allow them to register on your homeserver.

<img src="https://matrix.org/_matrix/media/v1/download/dmnd.sh/UKGgpbHRdFXzKywxjjbfHAsI" width="500">


## Setup
Install using pip:

```bash
pip3 install matrix-registration
```

or check the [docker guide](https://github.com/ZerataX/matrix-registration/wiki/docker)

### First start
To start, execute `matrix-registration`.

A configuration file should be generated for you on first start.

**Note:**
For `server_location` it is recommended to use a local connect, e.g. `localhost:8008` (or whatever port synapse listens to).
It is possible however to connect over the internet, but you will need to make sure `/_synapse/admin/v1/register` is accessible.

<details>
  <summary> If the configuration file is not automatically discovered...</summary>
  
you can create a configuration by copying [config.sample.yaml](/config.sample.yaml) to your server and editing it:
```bash
wget https://raw.githubusercontent.com/ZerataX/matrix-registration/master/config.sample.yaml
cp config.sample.yaml config.yaml
nano config.yaml
```

Then pass the path to this configuration to the application on startup using `--config-path /path/to/config.yaml`.
</details>

__INFO:__ 
- This only asks you for the most important options. 
You 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.

## Usage

```bash
$ matrix-registration -h
Usage: matrix-registration [OPTIONS] COMMAND [ARGS]...

  a token based matrix registration app

Options:
  --config-path TEXT  specifies the config file to be used
  --version           Show the flask version
  -h, --help          Show this message and exit.

Commands:
  generate  generate new token
  serve     start api server
  status    view status or disable

```

After 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:
- with a simple post request, e.g.:
```bash
curl -X POST \
     -F 'username=test' \
     -F 'password=verysecure' \
     -F 'confirm=verysecure' \
     -F 'token=DoubleWizardSki' \
     http://localhost:5000/register
```
- or by visiting http://localhost:5000/register?token=DoubleWizardSki


## Further Resources

### Nginx reverse-proxy

If you'd like to run matrix-registration behind a reverse-proxy, here is an example nginx setup:

```nginx
location  ~ ^/(static|register) {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://localhost:5000;
}
```

If 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)

### Custom registration page

If you want to write your own registration page, you can take a look at the sample in [resources/example.html](resources/example.html)

The 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.:

`https://homeserver.tld/register.html?token=DoubleWizardSki`

If 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.


### bot

if you're looking for a bot to interface with matrix-registration and manage your tokens, take a look at:

[maubot-invite](https://github.com/williamkray/maubot-invite)


### Similar projects

  - [matrix-invite](https://gitlab.com/reivilibre/matrix-invite) live at https://librepush.net/matrix/registration/
  - [matrix-register-bot](https://github.com/krombel/matrix-register-bot) using a bot to review accounts before sending out invite links
  - [MatrixRegistration](https://gitlab.com/olze/matrixregistration/) similar java project using my webui
  - [Mother Miounne](https://gitlab.com/etke.cc/miounne) "A bridge between matrix and external services", which also integrates matrix-registration

For more info check the [wiki](https://github.com/ZerataX/matrix-registration/wiki)

### Artwork attribution

- The valley cover photo on the registration page is photo by [Jesús Roncero](https://www.flickr.com/golan)
used under the terms of [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). No warranties are given.
- 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).


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions
This project is still in active develoment so be aware that only the latest version will receive security fixes.

| Version | Supported          |
| ------- | ------------------ |
| latest  | :white_check_mark: |
| < 1.0   | :x:                |


## Reporting a Vulnerability
If 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:

- What kind of issue is it (leaked data, skipped authentification, ...)
- How critical is the issue
- Steps to reproduce
- If possible: How can we contact you (E-Mail, matrix account)

If 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)


================================================
FILE: alembic/README
================================================


================================================
FILE: alembic/env.py
================================================
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

import sys
from os import getcwd
from os.path import abspath, dirname

sys.path.insert(0, dirname(dirname(abspath(__file__))))

from matrix_registration import config as mr_config


# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)

# load matrix-registration config and set db path for alembic
config_path = context.get_x_argument(as_dictionary=True).get("config") or "config.yaml"
mr_config.config = mr_config.Config(config_path)
config.set_main_option("sqlalchemy.url", mr_config.config.db.replace("{cwd}", f"{getcwd()}"))
print(config.get_main_option("sqlalchemy.url"))

# add your model's MetaData object here
# for 'autogenerate' support
# target_metadata = mymodel.Base.metadata
target_metadata = None

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline():
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            render_as_batch=config.get_main_option('sqlalchemy.url').startswith('sqlite:///')
        )

        with context.begin_transaction():
            context.run_migrations()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()


================================================
FILE: alembic/script.py.mako
================================================
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}


def upgrade():
    ${upgrades if upgrades else "pass"}


def downgrade():
    ${downgrades if downgrades else "pass"}


================================================
FILE: alembic/versions/130b5c2275d8_update_ip_token_association.py
================================================
'''update ip token association

Revision ID: 130b5c2275d8
Revises: 140a25d5f185
Create Date: 2021-07-10 20:40:46.937634

'''
from alembic import op
import sqlalchemy as sa
from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy.engine.reflection import Inspector
from flask_sqlalchemy import SQLAlchemy

# revision identifiers, used by Alembic.
revision = '130b5c2275d8'
down_revision = '140a25d5f185'
branch_labels = None
depends_on = None

db = SQLAlchemy()
conn = op.get_bind()

def upgrade():
        ips = conn.execute('select id, address from ips').fetchall()
        associations = conn.execute('select ips, tokens from association').fetchall()

        final_associations = []
        for association in associations:
            association_ip, association_token = association
            for ip in ips:
                id, ip_address = ip
                if ip_address == association_ip:
                    final_associations.append({'ips': id, 'tokens': association_token})

        op.drop_table('association')

        association = op.create_table(
            'association', db.Model.metadata,
                Column('ips', Integer, ForeignKey('ips.id'), primary_key=True),
                Column('tokens', String(255), ForeignKey('tokens.name'), primary_key=True)
        )   


        op.bulk_insert(association, final_associations)

        

def downgrade():
    ips = conn.execute('select id, address from ips').fetchall()
    associations = conn.execute('select ips, tokens from association').fetchall()

    final_associations = []
    for association in associations:
        association_ip, association_token = association
        for ip in ips:
            id, ip_address = ip
            if id == association_ip:
                final_associations.append({'ips': ip_address, 'tokens': association_token})

    op.drop_table('association')

    association = op.create_table(
        'association', db.Model.metadata,
            Column('ips', String(255), ForeignKey('ips.address'), primary_key=True),
            Column('tokens', String(255), ForeignKey('tokens.name'), primary_key=True)
    )   


    op.bulk_insert(association, final_associations)


================================================
FILE: alembic/versions/140a25d5f185_create_tokens_table.py
================================================
"""create tokens table

Revision ID: 140a25d5f185
Revises: 
Create Date: 2020-12-12 01:44:28.195736

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import Table, Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.engine.reflection import Inspector
from flask_sqlalchemy import SQLAlchemy


# revision identifiers, used by Alembic.
revision = '140a25d5f185'
down_revision = None
branch_labels = None
depends_on = None

db = SQLAlchemy()


def upgrade():
    conn = op.get_bind()
    inspector = Inspector.from_engine(conn)
    tables = inspector.get_table_names()

    if 'ips' not in tables:
        op.create_table(
            'ips',
            sa.Column('id', sa.Integer, primary_key=True),
            sa.Column('address', sa.String(255), nullable=True)
        )

    if 'tokens' not in tables:
        op.create_table(
            'tokens',
            sa.Column('name', String(255), primary_key=True),
            sa.Column('expiration_date', DateTime, nullable=True),
            sa.Column('max_usage', Integer, default=1),
            sa.Column('used', Integer, default=0),
            sa.Column('disabled', Boolean, default=False),
            sa.Column('ips', Integer, ForeignKey('association.id'))
        )
    else:
        try:
            with op.batch_alter_table('tokens') as batch_op:
                batch_op.alter_column('ex_date', new_column_name='expiration_date', nullable=True)
                batch_op.alter_column('one_time', new_column_name='max_usage')

                batch_op.add_column(
                    Column('disabled', Boolean, default=False)
                )
        except KeyError:
            pass


    if 'association' not in tables:
        op.create_table(
        'association', db.Model.metadata,
            Column('ips', String, ForeignKey('ips.address'), primary_key=True),
            Column('tokens', Integer, ForeignKey('tokens.name'), primary_key=True)
        )   
    
    op.execute("update tokens set expiration_date=null where expiration_date='None'")



def downgrade():
    op.alter_column('tokens', 'expiration_date', new_column_name='ex_date')
    op.alter_column('tokens', 'max_usage', new_column_name='one_time')


================================================
FILE: alembic.ini
================================================
# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = alembic

# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =

# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; this defaults
# to alembic/versions.  When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S


================================================
FILE: config.sample.yaml
================================================
server_location: 'http://localhost:8008'
server_name: 'matrix.org'
registration_shared_secret: 'RegistrationSharedSecret' # see your synapse's homeserver.yaml
admin_api_shared_secret: 'APIAdminPassword' # to generate tokens via the web api
base_url: '' # e.g. '/element' for https://example.tld/element/register
client_redirect: 'https://app.element.io/#/login'
client_logo: 'static/images/element-logo.png' # use '{cwd}' for current working directory
db: 'sqlite:///{cwd}/db.sqlite3'
host: 'localhost'
port: 5000
rate_limit: ["100 per day", "10 per minute"]
allow_cors: false
ip_logging: false
logging:
  disable_existing_loggers: false
  version: 1
  root:
    level: DEBUG
    handlers: [console, file]
  formatters:
    brief:
      format: '%(name)s - %(levelname)s - %(message)s'
    precise:
      format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  handlers:
    console:
      class: logging.StreamHandler
      level: INFO
      formatter: brief
      stream: ext://sys.stdout
    file:
      class: logging.handlers.RotatingFileHandler
      formatter: precise
      level: INFO
      filename: m_reg.log
      maxBytes: 10485760 # 10MB
      backupCount: 3
      encoding: utf8
# password requirements
password:
  min_length: 8
# username requirements
username:
  validation_regex: [] #list of regexes that the selected username must match.        Example: '[a-zA-Z]\.[a-zA-Z]'
  invalidation_regex: [] #list of regexes that the selected username must NOT match.  Example: '(admin|support)'


================================================
FILE: default.nix
================================================
{ pkgs ? import <nixpkgs> { } }:
with pkgs.python3.pkgs;

buildPythonPackage rec {
  name = "matrix-registration";
  src = builtins.path {
    inherit name;
    path = ./.;
  };

  postPatch = ''
    sed -i -e '/alembic>/d' setup.py
  '';

  propagatedBuildInputs = [
    appdirs
    flask
    flask-cors
    flask-httpauth
    flask-limiter
    flask_sqlalchemy
    jsonschema
    python-dateutil
    pyyaml
    requests
    waitress
    wtforms
    psycopg2
  ];

  checkInputs = [ parameterized ];
}


================================================
FILE: docker.nix
================================================
{ pkgs ? import <nixpkgs> { }, tag ? "latest" }:

let
  matrix-registration-config = "/data/config.yaml";

  python3 = let
    packageOverrides = self: super: rec {
      alembic = super.alembic.overridePythonAttrs (old: {
        makeWrapperArgs = [
          "--chdir '${matrix-registration}'"
          ''--add-flags "-x config='${matrix-registration-config}'"''
        ];
      });
      matrix-registration =
        (import ./default.nix { inherit pkgs; }).overridePythonAttrs (old: {
          makeWrapperArgs =
            [ ''--add-flags "--config-path='${matrix-registration-config}'"'' ];
        });
    };
  in pkgs.python3.override {
    inherit packageOverrides;
    # enableOptimizations = true;
    # reproducibleBuild = false;
    self = python3;
  };

  python-packages = ps: with ps; [ matrix-registration alembic ];

in pkgs.dockerTools.buildImage {
  name = "matrix-registration";
  tag = tag;
  created = "now";

  copyToRoot = python3.withPackages python-packages;

  config = {
    CMD = [ "matrix-registration" "serve"];
    WorkingDir = "/data";
    Volumes = { "/data" = { }; };
    ExposedPorts = { "5000/tcp" = { }; };
    HealthCheck = {
      Interval = 3000000000;
      Timeout = 1000000000;
      StartPeriod = 3000000000;
      Test =
        [ "CMD" "${pkgs.curl}/bin/curl" "-fSs" "http://localhost:5000/health" ];
    };
  };
}


================================================
FILE: matrix_registration/__init__.py
================================================
from . import api
from . import tokens
from . import config

__version__ = "0.9.2.dev3"
name = "matrix_registration"


================================================
FILE: matrix_registration/api.py
================================================
# Standard library imports...
import logging
import os
import re
from datetime import datetime

# Third-party imports...
from flask import (
    Blueprint,
    abort,
    jsonify,
    request,
    make_response,
    render_template,
    send_file,
)
from flask_httpauth import HTTPTokenAuth
from requests import exceptions
from werkzeug.exceptions import BadRequest
from wtforms import Form, StringField, PasswordField, validators

# Local imports...
from . import config
from . import tokens
from .constants import __location__
from .limiter import limiter, get_default_rate_limit
from .matrix_api import create_account
from .translation import get_translations

auth = HTTPTokenAuth(scheme="SharedSecret")
logger = logging.getLogger(__name__)

api = Blueprint("api", __name__)
healthcheck = Blueprint("healthcheck", __name__)
limiter.limit(get_default_rate_limit)(api)


def validate_token(form, token):
    """
    validates token

    Parameters
    ----------
    arg1 : Form object
    arg2 : str
        token name, e.g. 'DoubleWizardSki'

    Raises
    -------
    ValidationError
        Token is invalid

    """
    tokens.tokens.load()
    if not tokens.tokens.active(token.data):
        raise validators.ValidationError("Token is invalid")


def validate_username(form, username):
    """
    validates username

    Parameters
    ----------
    arg1 : Form object
    arg2 : str
        username name, e.g: '@user:matrix.org' or 'user'
        https://github.com/matrix-org/matrix-doc/blob/master/specification/appendices/identifier_grammar.rst#user-identifiers
    Raises
    -------
    ValidationError
        Username doesn't follow mxid requirements
    """
    re_mxid = f"^(?P<at>@)?(?P<username>[a-zA-Z_\-=\.\/0-9]+)(?P<server_name>:{re.escape(config.config.server_name)})?$"
    match = re.search(re_mxid, username.data)
    if not match:
        raise validators.ValidationError(
            f"Username doesn't follow mxid pattern: /{re_mxid}/"
        )
    username = match.group("username")
    for e in [
        validators.ValidationError(f"Username does not follow custom pattern /{x}/")
        for x in config.config.username["validation_regex"]
        if not re.search(x, username)
    ]:
        raise e
    for e in [
        validators.ValidationError(f"Username must not follow custom pattern /{x}/")
        for x in config.config.username["invalidation_regex"]
        if re.search(x, username)
    ]:
        raise e


def validate_password(form, password):
    """
    validates username

    Parameters
    ----------
    arg1 : Form object
    arg2 : str
        password
    Raises
    -------
    ValidationError
        Password doesn't follow length requirements
    """
    min_length = config.config.password["min_length"]
    err = "Password should be between %s and 255 chars long" % min_length
    if len(password.data) < min_length or len(password.data) > 255:
        raise validators.ValidationError(err)


class RegistrationForm(Form):
    """
    Registration Form

    validates user account registration requests
    """

    username = StringField(
        "Username",
        [
            validators.Length(min=1, max=200),
            # validators.Regexp(re_mxid)
            validate_username,
        ],
    )
    password = PasswordField(
        "New Password",
        [
            # validators.Length(min=8),
            validate_password,
            validators.DataRequired(),
            validators.EqualTo("confirm", message="Passwords must match"),
        ],
    )
    confirm = PasswordField("Repeat Password")
    token = StringField(
        "Token", [validators.Regexp(r"^([A-Z][a-z]+)+$"), validate_token]
    )


def get_request_ips(request):
    """
    Get the chain of client and proxy IP addresses from the request as
    a nonempty list, where the closest IP in the chain is last. Each
    IP vouches only for the IP before it. This works best if all proxies
    conform the to the X-Forwarded-For header spec, including whatever
    reverse proxy (such as nginx) is directly in front of the app, if any.
    (X-Real-IP and similar are not supported at this time.)
    """
    return request.headers.getlist("X-Forwarded-For") + [request.remote_addr]


@auth.verify_token
def verify_token(token):
    return (
        token != "APIAdminPassword" and token == config.config.admin_api_shared_secret
    )


@auth.error_handler
def unauthorized():
    resp = {"errcode": "MR_BAD_SECRET", "error": "wrong shared secret"}
    return make_response(jsonify(resp), 401)


@api.route("/static/replace/images/element-logo.png")
def element_logo():
    return send_file(
        config.config.client_logo.replace("{cwd}", f"{os.getcwd()}/"),
        mimetype="image/jpeg",
    )


@api.route("/register", methods=["GET", "POST"])
def register():
    """
    main user account registration endpoint
    to register an account you need to send a
    application/x-www-form-urlencoded request with
      - username
      - password
      - confirm
      - token
    as described in the RegistrationForm
    """
    if request.method == "POST":
        logger.debug("an account registration started...")
        form = RegistrationForm(request.form)
        logger.debug("validating request data...")
        if form.validate():
            logger.debug("request valid")
            return create_account_from_form(form)

        logger.debug("account creation failed!")
        resp = {"errcode": "MR_BAD_USER_REQUEST", "error": form.errors}
        return make_response(jsonify(resp), 400)

    # GET REQUEST
    server_name = config.config.server_name
    pw_length = config.config.password["min_length"]
    uname_regex = config.config.username["validation_regex"]
    uname_regex_inv = config.config.username["invalidation_regex"]
    lang = request.args.get("lang") or request.accept_languages.best
    replacements = {"server_name": server_name, "pw_length": pw_length}
    translations = get_translations(lang, replacements)
    return render_template(
        "register.html",
        server_name=server_name,
        pw_length=pw_length,
        uname_regex=uname_regex,
        uname_regex_inv=uname_regex_inv,
        client_redirect=config.config.client_redirect,
        base_url=config.config.base_url,
        translations=translations,
    )


def create_account_from_form(form):
    # remove sigil and the domain from the username
    username = form.username.data.rsplit(":")[0].split("@")[-1]
    logger.debug("creating account %s..." % username)
    # send account creation request to the hs
    try:
        account_data = create_account(
            form.username.data,
            form.password.data,
            config.config.server_location,
            config.config.registration_shared_secret,
        )
    except exceptions.ConnectionError:
        logger.error(
            "can not connect to %s" % config.config.server_location,
            exc_info=True,
        )
        abort(500)
    except exceptions.HTTPError as e:
        resp = e.response
        error = resp.json()
        status_code = resp.status_code
        if status_code == 404:
            logger.error("no HS found at %s" % config.config.server_location)
        elif status_code == 403:
            logger.error("wrong shared registration secret or not enabled")
        elif status_code == 400:
            # most likely this should only be triggered if a userid
            # is already in use
            return make_response(jsonify(error), 400)
        else:
            logger.error("failure communicating with HS", exc_info=True)
        abort(500)

    logger.debug("using token %s" % form.token.data)
    ips = ", ".join(get_request_ips(request)) if config.config.ip_logging else False
    tokens.tokens.use(form.token.data, ips)

    logger.debug("account creation succeded!")
    return jsonify(
        access_token=account_data["access_token"],
        home_server=account_data["home_server"],
        user_id=account_data["user_id"],
        status="success",
        status_code=200,
    )


def get_token(token):
    if tokens.tokens.get_token(token):
        return jsonify(tokens.tokens.get_token(token).toDict())
    resp = {"errcode": "MR_TOKEN_NOT_FOUND", "error": "token does not exist"}
    return make_response(jsonify(resp), 404)


def get_tokens():
    return jsonify(tokens.tokens.toList())


def create_token(data):
    if not data:
        resp = {
            "errcode": "MR_BAD_USER_REQUEST",
            "error": "no data was sent",
        }
        return make_response(jsonify(resp), 400)

    max_usage = False
    expiration_date = None
    try:

        if "expiration_date" in data and data["expiration_date"] is not None:
            expiration_date = datetime.fromisoformat(data["expiration_date"])
        if "max_usage" in data:
            max_usage = data["max_usage"]
        token = tokens.tokens.new(expiration_date=expiration_date, max_usage=max_usage)
    except ValueError:
        resp = {
            "errcode": "MR_BAD_DATE_FORMAT",
            "error": "date wasn't in YYYY-MM-DD format",
        }
        return make_response(jsonify(resp), 400)
    return jsonify(token.toDict())


def update_token(token, data):
    if "ips" in data or "active" in data or "name" in data:
        resp = {
            "errcode": "MR_BAD_USER_REQUEST",
            "error": "you're not allowed to change this property",
        }
        return make_response(jsonify(resp), 400)
    if tokens.tokens.update(token, data):
        return jsonify(tokens.tokens.get_token(token).toDict())

    resp = {"errcode": "MR_TOKEN_NOT_FOUND", "error": "token does not exist"}
    return make_response(jsonify(resp), 404)


def delete_token(token):
    if not tokens.tokens.get_token(token):
        resp = {"errcode": "MR_TOKEN_NOT_FOUND", "error": "token does not exist"}
        return (jsonify(resp), 404)
    if tokens.tokens.delete(token):
        resp = {"success": "true"}
        return make_response(jsonify(resp), 200)

    resp = {"success": "false"}
    return make_response(jsonify(resp), 500)


@healthcheck.route("/health")
def health():
    return make_response("OK", 200)


@api.route("/api/version")
@auth.login_required
def version():
    with open(os.path.join(__location__, "__init__.py"), "r") as file:
        version_file = file.read()
        version_match = re.search(
            r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M
        )
        resp = {"version": version_match.group(1)}
        return make_response(jsonify(resp), 200)


@api.route("/api/token", methods=["GET", "POST"])
@auth.login_required
def token():
    tokens.tokens.load()
    if request.method == "GET":
        return get_tokens()
    elif request.method == "POST":
        return create_token(request.get_json())

    resp = {"errcode": "MR_BAD_USER_REQUEST", "error": "malformed request"}
    return make_response(jsonify(resp), 400)


@api.route("/api/token/<token>", methods=["GET", "PATCH", "DELETE"])
@auth.login_required
def token_status(token):
    tokens.tokens.load()
    data = False
    if request.method == "GET":
        return get_token(token)

    elif request.method == "PATCH":
        return update_token(token, request.get_json())

    elif request.method == "DELETE":
        return delete_token(token)

    resp = {"errcode": "MR_BAD_USER_REQUEST", "error": "malformed request"}
    return make_response(jsonify(resp), 400)


================================================
FILE: matrix_registration/app.py
================================================
import json
import logging
import logging.config
import os

import click
from flask import Flask
from flask.cli import FlaskGroup, pass_script_info
from flask_cors import CORS
from waitress import serve

from . import config
from . import tokens
from .limiter import limiter
from .tokens import db


def create_app(testing=False):
    app = Flask(__name__)
    app.testing = testing

    with app.app_context():
        from .api import api, healthcheck

        app.register_blueprint(api)
        app.register_blueprint(healthcheck)

    limiter.init_app(app)
    return app


@click.group(
    cls=FlaskGroup,
    add_default_commands=False,
    create_app=create_app,
    context_settings=dict(help_option_names=["-h", "--help"]),
)
@click.option("--config-path", help="specifies the config file to be used")
@pass_script_info
def cli(info, config_path):
    """a token based matrix registration app"""
    config.config = config.Config(path=config_path)
    logging.config.dictConfig(config.config.logging)
    app = info.load_app()
    with app.app_context():
        app.config.from_mapping(
            SQLALCHEMY_DATABASE_URI=config.config.db.format(cwd=f"{os.getcwd()}"),
            SQLALCHEMY_TRACK_MODIFICATIONS=False,
        )
        db.init_app(app)
        db.create_all()
        tokens.tokens = tokens.Tokens()


@cli.command("serve", help="start api server")
@pass_script_info
def run_server(info):
    app = info.load_app()
    if config.config.allow_cors:
        CORS(app)
    serve(
        app,
        host=config.config.host,
        port=config.config.port,
        url_prefix=config.config.base_url,
    )


@cli.command("generate", help="generate new token")
@click.option("-m", "--maximum", default=0, help="times token can be used")
@click.option(
    "-e",
    "--expires",
    type=click.DateTime(formats=["%Y-%m-%d"]),
    default=None,
    help="expire date: in ISO-8601 format (YYYY-MM-DD)",
)
def generate_token(maximum, expires):
    token = tokens.tokens.new(expiration_date=expires, max_usage=maximum)
    print(token.name)


@cli.command("status", help="view status or disable")
@click.option("-s", "--status", default=None, help="token status")
@click.option("-l", "--list", is_flag=True, help="list tokens")
@click.option("-d", "--disable", default=None, help="disable token")
def status_token(status, list, disable):
    if disable:
        if tokens.tokens.disable(disable):
            print("Token disabled")
        else:
            print("Token couldn't be disabled")
    elif status:
        token = tokens.tokens.get_token(status)
        if token:
            print(f"This token is{' ' if token.active() else ' not '}valid")
            print(json.dumps(token.toDict(), indent=2))
        else:
            print("No token with that name")
    elif list:
        print(tokens.tokens)


================================================
FILE: matrix_registration/config.py
================================================
# Standard library imports...
# from collections import namedtuple
import logging
import os
import sys

# Third-party imports...
import yaml
from jsonschema import validate, ValidationError

# Local imports...
from .constants import (
    CONFIG_SCHEMA_PATH,
    CONFIG_DIR1,
    CONFIG_DIR2,
    CONFIG_DIR3,
    CONFIG_DIR4,
    CONFIG_DIR5,
)

CONFIG_DIRS = [CONFIG_DIR1, CONFIG_DIR2, CONFIG_DIR3, CONFIG_DIR4, CONFIG_DIR5]

CONFIG_SAMPLE_NAME = "config.sample.yaml"
CONFIG_NAME = "config.yaml"
logger = logging.getLogger(__name__)


class Config:
    """
    Config

    loads a dict or a yaml file to be accessible by all files in the module
    """

    def __init__(self, path=None, data=None):
        self.secrets_dir = os.getenv("CREDENTIALS_DIRECTORY")
        self.data = data
        self.path = path
        self.default = True

        self.load()
        if self.secrets_dir:
            self.load_secrets()
        self.apply_options()

    def load(self):
        """
        loads the options
        """
        logger.debug("loading config...")
        if self.data:
            logger.debug("from dict...")
            config_default = False
            return

        logger.debug("from file...")
        self.load_from_file()

    def load_from_file(self):
        """
        loads the options from a file
        """
        options = None
        if not self.check_config_locations():
            sys.exit("could not find any configuration file!")
        logger.debug(f"config found!")

        try:
            with open(self.path, "r") as stream:
                options = yaml.load(stream, Loader=yaml.SafeLoader)
            with open(CONFIG_SCHEMA_PATH, "r") as schemafile:
                validate(options, yaml.safe_load(schemafile))
        except ValidationError as e:
            sys.exit(
                "Check you config and update it to the newest version! Do you have missing fields in your config.yaml?\n\nTraceback:\n"
                + str(e)
            )
        except yaml.YAMLError as e:
            sys.exit("Invalid YAML Syntax\n\nTraceback:\n" + str(e))
        except IOError as e:
            sys.exit(e)

        if not options:
            sys.exit("could not read file")

        if self.default:
            # ask for options that should not be set to default
            options = self.ask_for_options(options)

        self.data = options

    def load_secrets(self):
        """
        loads secret options, see https://systemd.io/CREDENTIALS/
        """
        with open(f"{self.secrets_dir}/secrets") as file:
            for line in file:
                try:
                    k, v = line.lower().split("=")
                except NameError:
                    logger.error(
                        f'secret "{line}" in wrong format, please use "key=value"'
                    )
                setattr(self, k.strip(), v.strip())

    def check_config_locations(self):
        """
        checks multiple locations for the config or config sample file
        """
        if self.path:
            logger.debug(f"checking {self.path} ...")
            if os.path.isfile(self.path):
                self.default = False
                return True
            else:
                sys.exit("no configuration file at specified location")

        # check possible locations for config file
        for directory in CONFIG_DIRS:
            logger.debug(f"checking {directory} ...")
            path = directory + CONFIG_SAMPLE_NAME
            if os.path.isfile(path):
                self.path = path
                self.default = False
                return True

        # no config exists, use sample config instead
        # check typical installation dirs for sample configs
        for directory in CONFIG_DIRS:
            path = directory + CONFIG_SAMPLE_NAME
            if os.path.isfile(directory + CONFIG_SAMPLE_NAME):
                self.path = path
                self.config_exists = True
                return True

        return False

    def apply_options(self):
        """
        applies options to the config object
        """
        logger.debug("applying options...")
        # recusively set dictionary to class properties
        for k, v in self.data.items():
            setattr(self, k, v)

    def ask_for_options(self, sample_options):
        """
        asks the user how to set the essential options

        Parameters
        ----------
        arg1 : dict
            with default values
        """
        # important keys that need to be changed
        keys = ["server_location", "server_name", "port", "registration_shared_secret"]
        for key in keys:
            temp = sample_options[key]
            sample_options[key] = input("enter {}, e.g. {}\n".format(key, temp))
            if not sample_options[key].strip():
                sample_options[key] = temp

        return sample_options

        # write to config file
        directory = os.path.dirname(os.path.realpath(self.path))
        new_path = f"{directory}/{CONFIG_NAME}"
        with open(new_path, "w") as stream:
            yaml.dump(self.data, stream, default_flow_style=False)
            print(f'config file written to "{os.path.relpath(new_path)}"')
            print()

    def update(self, data):
        """
        resets all options and loads the new config

        Parameters
        ----------
        arg1 : dict or path to config file
        """
        logger.debug("updating config...")
        self.data = data
        self.path = None
        self.load()
        if self.secrets_dir:
            self.load_secrets()
        self.apply_options()
        logger.debug("config updated!")


config = None


================================================
FILE: matrix_registration/config.schema.json
================================================
{
  "type": "object",
  "properties": {
    "server_location": {
      "type": "string",
      "format": "uri",
      "pattern": "^https?://"
    },
    "server_name": {
      "type": "string"
    },
    "registration_shared_secret": {
      "type": "string"
    },
    "admin_api_shared_secret": {
      "type": "string"
    },
    "base_url": {
      "type": "string"
    },
    "client_redirect": {
      "type": "string"
    },
    "client_logo": {
      "type": "string"
    },
    "db": {
      "type": "string"
    },
    "host": {
      "type": "string"
    },
    "port": {
      "oneOf": [
        {"type": "integer"},
        {"type": "string", "pattern": "^/d+$"}
      ]
    },
    "rate_limit": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "allow_cors": {
      "type": "boolean"
    },
    "ip_logging": {
      "type": "boolean"
    },
    "logging": {
      "type": "object"
    },
    "password": {
      "type": "object",
      "properties": {
        "min_length": {
          "type": "integer"
        }
      },
      "required": [
        "min_length"
      ]
    },
    "username": {
      "type": "object",
      "properties": {
        "validation_regex": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "invalidation_regex": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      },
      "required": [
        "validation_regex",
        "invalidation_regex"
      ]
    }
  },
  "required": [
    "server_location",
    "server_name",
    "registration_shared_secret",
    "admin_api_shared_secret",
    "base_url",
    "client_redirect",
    "client_logo",
    "db",
    "host",
    "port",
    "rate_limit",
    "allow_cors",
    "ip_logging",
    "logging",
    "password",
    "username"
  ]
}


================================================
FILE: matrix_registration/constants.py
================================================
# Standard library imports...
import os
import site
import sys

# Third-party imports...
from appdirs import user_config_dir

__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
WORD_LIST_PATH = os.path.join(__location__, "wordlist.txt")
CONFIG_SCHEMA_PATH = os.path.join(__location__, "config.schema.json")
# first check in current working dir
CONFIG_DIR1 = os.path.join(os.getcwd() + "/")
CONFIG_DIR2 = os.path.join(os.getcwd() + "/config/")
# then check in XDG_CONFIG_HOME
CONFIG_DIR3 = os.path.join(user_config_dir("matrix-registration") + "/")
# check at installed location
CONFIG_DIR4 = os.path.join(__location__, "../")
CONFIG_DIR5 = os.path.join(sys.prefix, "config/")


================================================
FILE: matrix_registration/limiter.py
================================================
from flask import request
from flask_limiter import Limiter

from . import config


def get_real_user_ip() -> str:
    """ratelimit the users original ip instead of (optional) reverse proxy"""
    return next(iter(request.headers.getlist("X-Forwarded-For")), request.remote_addr)


def get_default_rate_limit() -> str:
    """return limit_string"""
    return "; ".join(config.config.rate_limit)


limiter = Limiter(key_func=get_real_user_ip)


================================================
FILE: matrix_registration/matrix_api.py
================================================
# Standard library imports...
import hashlib
import hmac
import requests

import logging

logger = logging.getLogger(__name__)


def create_account(
    user, password, server_location, shared_secret, admin=False, user_type=None
):
    """
    creates account
    https://github.com/matrix-org/synapse/blob/master/synapse/_scripts/register_new_matrix_user.py

    Parameters
    ----------
    arg1 : str
        local part of the new user
    arg2 : str
        password
    arg3 : str
        url to homeserver
    arg4 : str
        Registration Shared Secret as set in the homeserver.yaml
    arg5 : bool
        register new user as an admin.
    Raises
    -------
    requests.exceptions.ConnectionError:
        can't connect to homeserver
    requests.exceptions.HTTPError:
        something with the communciation to the homeserver failed
    """
    nonce = _get_nonce(server_location)

    mac = hmac.new(key=shared_secret.encode("utf8"), digestmod=hashlib.sha1)

    mac.update(nonce.encode("utf8"))
    mac.update(b"\x00")
    mac.update(user.encode("utf8"))
    mac.update(b"\x00")
    mac.update(password.encode("utf8"))
    mac.update(b"\x00")
    mac.update(b"admin" if admin else b"notadmin")
    if user_type:
        mac.update(b"\x00")
        mac.update(user_type.encode("utf8"))

    mac = mac.hexdigest()

    data = {
        "nonce": nonce,
        "username": user,
        "password": password,
        "mac": mac,
        "admin": admin,
        "user_type": user_type,
    }

    server_location = server_location.rstrip("/")

    r = requests.post("%s/_synapse/admin/v1/register" % (server_location), json=data)
    r.raise_for_status()
    return r.json()


def _get_nonce(server_location):
    r = requests.get("%s/_synapse/admin/v1/register" % (server_location))
    r.raise_for_status()
    return r.json()["nonce"]


================================================
FILE: matrix_registration/static/css/style.css
================================================
html,
body {
  height: 100%;
  margin: 0;
  font-family: 'Nunito', sans-serif;
}

body {
  background-size: cover;
  background-attachment: fixed;
  overflow: hidden;
}

h1 {
  font-size: 1.3em;
}

article {
  color: white;
}

a:link,
a:visited {
  color: #038db3 !important;
}

form {
  width: 320px;
  margin: 45px auto;
}

textarea {
  resize: none;
}

input,
textarea {
  background: none;
  color: white;
  font-size: 18px;
  padding: 10px 10px 10px 5px;
  display: block;
  width: 320px;
  border: none;
  border-radius: 0;
  border-bottom: 1px solid white;
}

input:focus,
textarea:focus {
  outline: none;
}

input:focus~label,
input:not(:placeholder-shown)~label,
textarea:focus~label,
textarea:valid~label {
  top: -14px;
  font-size: 12px;
  color: #03b381;
}

input:focus~.bar:before,
textarea:focus~.bar:before {
  width: 320px;
}

input[type="password"] {
  letter-spacing: 0.3em;
}

input:invalid {
  box-shadow: none;
}

input:invalid~.bar:before {
  background: #038db3;
}

input:invalid~label {
  color: #038db3;
}

input[type="submit"] {
  cursor: pointer;
}

label {
  color: white;
  font-size: 16px;
  font-weight: normal;
  position: absolute;
  pointer-events: none;
  left: 5px;
  top: 10px;
  transition: 300ms ease all;
}

*,
:before,
:after {
  box-sizing: border-box;
}

.center {
  text-align: center;
  margin-top: 2em;
}
.hidden {
  visibility: hidden;
  opacity: 0;
}

.group {
  position: relative;
  margin: 45px 0;
}

.bar {
  position: relative;
  display: block;
  width: 320px;
}

.bar:before {
  content: '';
  height: 2px;
  width: 0;
  bottom: 0px;
  position: absolute;
  background: #03b381;
  transition: 300ms ease all;
  left: 0%;
}

.btn {
  background: white;
  color: black;
  border: none;
  padding: 10px 20px;
  border-radius: 3px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  outline: none;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}

.btn:hover {
  color: black;
  box-shadow: 0 7px 14px rgba(0, 0, 0, 0.18), 0 5px 5px rgba(0, 0, 0, 0.12);
}

.btn.btn-submit {
  background: #03b381;
  color: #bce0fb;
}

.btn.btn-submit:hover {
  background: #03b372;
  color: #deeffd;
}

.btn-box {
  text-align: center;
  margin: 50px 0;
}

.info {
  z-index: 2;
  position: absolute;
  bottom: .5vh;
  right: 1vw;
  text-align: left;
  color: grey;
  font-size: 0.8em;
  opacity: 0.1;
  transition: opacity 0.5s ease;
}

.info:hover {
  opacity: 1;
}

.info a {
  color: cyan;
}

.widget {
  position: absolute;
  left: 50%;
  top: 50%;
  border: 0px solid;
  border-radius: 5px;
  overflow: hidden;
  background-color: #1f1f1f;
  z-index: 1;
  box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.5);
}

.widget::before {
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  background-attachment: fixed;
  background-size: cover;
  opacity: 0.20;
  content: "";
}

.blur:before {
  content: "";
  position: absolute;
  width: 100%;
  height: 100%;
  background: inherit;
  z-index: -1;
  transform: scale(1.03);
  filter: blur(10px);
}

.register {
  margin-left: -15em;
  margin-top: -20em;
  width: 30em;
  height: 40em;
}

.modal {
  margin-left: -12.5em;
  margin-top: -7.5em;
  width: 25em;
  background-color: #f7f7f7;
  transition: visibility .3s, opacity .3s linear;
}

.modal article {
  margin-top: -5em;
}

.modal article,
.modal p,
.modal h2,
.modal h3 {
  color: #1f1f1f;
}

.error {
  color: #b30335 !important;
}

@media only screen and (max-width: 500px) {
  .info {
    bottom: -2vh;
  }

  .widget {
    margin-top: -40vh;
    margin-left: -45vw;
    width: 90vw;
    min-width: 20em;
  }

  .modal {
    margin-top: -15vh;
    margin-left: -35vw;
    width: 70vw;
    min-width: 15em;
  }
}

@media only screen and (max-height: 768px) {
  body {
    overflow-y: visible;
    padding-bottom: -90vh;
  }

  .blur:before {
    filter: none;
    transform: none;
    padding-bottom: 50em;
  }

  .info {
    float: right;
    padding-top: 57em;
    position: static;
  }

  .widget {
    margin-top: -40vh;
  }

  .modal {
    margin-top: -15vh;
  }
}

================================================
FILE: matrix_registration/static/fonts/NUNITO-LICENSE
================================================
Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.


================================================
FILE: matrix_registration/templates/register.html
================================================
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width initial-scale=1.0" />
  <meta property="og:title" content="{{ translations.server_registration }}">
  <meta property="og:site_name" content="{{ server_name }}">
  <meta property="og:type" content="website" />
  <meta name="og:description" content="{{ translations.register_account }}"/>
  <meta name="og:image" content="{{ url_for('static', filename='images/icon.png') }}" />
  <link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='images/icon.png') }}">
  <link rel="icon" type="image/png" href="{{ url_for('static', filename='images/icon32x32.png') }}" sizes="32x32">
  <link rel="preload" as="image" href="{{ url_for('static', filename='images/favicon.ico') }}">
  <link rel="shortcut icon" href="{{ url_for('static', filename='images/favicon.ico') }}">
  <meta name="msapplication-TileImage" content="{{ url_for('static', filename='images/tile.png') }}">
  <meta name="msapplication-TileColor" content="#fff">
  <title>{{ translations.server_registration }}</title>
  <!-- font designed by Vernon Adams, Cyreal -->
  <!-- https://fonts.google.com/specimen/Nunito -->
  <!-- licensed under SIL Open Font License, Version 1.1 -->
  <link rel="preload" as="font" href="{{ url_for('static', filename='fonts/Nunito_400_Nunito_700.woff2') }}" type="font/woff2" crossorigin="anonymous">
  <link rel="preload" as="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
  <!-- valley cover by Designed by Jesús Roncero -->
  <!-- https://www.flickr.com/golan -->
  <!-- licensed under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/) -->
  <link rel="preload" as="image" href="{{ url_for('static', filename='images/valley.jpg') }}">
  <style>
    @font-face {
      font-family: "Nunito";
      src: local("Nunito"), url("{{ url_for('static', filename='fonts/Nunito_400_Nunito_700.woff2') }}");
    }
    body, .widget::before {
      background-image: url("{{ url_for('static', filename='images/valley.jpg') }}");
    }
  </style>
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>

<body class="blur">
  <article class="widget register">
    <div class="center">
      <header>
        <h1>{{ translations.server_registration }}</h1>
        <p>{{ translations.requires_token }}<br>
          {{ translations.requires_username_and_password }}</p>
      </header>
      <section>
        <form id="registration" action="{{ base_url }}/register" method="post">
          <div class="group">
            <input id="username" name="username" type="text" placeholder=" "
              pattern="^@?[a-zA-Z_\-=\.\/0-9]+(:{{ server_name|replace('.', '\.') }})?$" 
              minlength="1" maxlength="200" required>
            <span class="highlight"></span>
            <span class="bar"></span>
            <label for="username">{{ translations.username }}</label>
          </div>
          <div class="group">
            <input id="password" name="password" type="password" placeholder=" " required minlength="{{ pw_length }}"
              maxlength="128">
            <span class="highlight"></span>
            <span class="bar"></span>
            <label for="password">{{ translations.password }}</label>
          </div>
          <div class="group">
            <input id="confirm_password" name="confirm" type="password" placeholder=" " required>
            <span class="highlight"></span>
            <span class="bar"></span>
            <label for="confirm_password">{{ translations.confirm }}</label>
          </div>
          <div class="group">
            <input id="token" name="token" type="text" placeholder=" " required pattern="^([A-Z][a-z]+)+$">
            <span class="highlight"></span>
            <span class="bar"></span>
            <label for="token">{{ translations.token }}</label>
          </div>
          <div class="btn-box">
            <input class="btn btn-submit" type="submit" value="{{ translations.register }}">
          </div>
        </form>
      </section>
    </div>
  </article>
  <article id="success" class="widget modal hidden">
    <div class="center">
      <header>
        <h2 id="welcome"></h2>
      </header>
      <section>
        <p> {{ translations.click_to_login }}</p>
        <h3><a href="{{ client_redirect }}"><img src="{{ base_url }}/static/replace/images/element-logo.png" height="100px"></a></h3>
        <p>{{ translations.choose_client }} <a href="https://matrix.org/docs/projects/clients-matrix"
            a>https://matrix.org/docs/projects/clients-matrix</a></p>
      </section>
    </div>
  </article>
  <article id="error" class="widget modal hidden">
    <div class="center">
      <header>
        <h2>{{ translations.error }}</h2>
      </header>
      <section>
        <p>{{ translations.error_long }}</p>
        <h3 id="error_message" class="error"></h3>
        <p id="error_dialog"></p>
      </section>
    </div>
  </article>
  <footer class="info">
    <p>Cover photo by: <a href="https://www.flickr.com/golan" target="_blank">Jesús Roncero</a>,<br>
      used under the terms of <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC-BY-SA
        4.0</a>. No warranties are given.
    </p>
  </footer>

  <script>
    // all javascript here is optional, the registration form works fine without
    /* 
    What this script does:
      - confirm password validator needs javascript, otherwise always valid as long as not empty
      - set token with ?token query parameter
      - set custom validity messages
    */

    // see https://stackoverflow.com/a/3028037
    function hideOnClickOutside(element) {
      const outsideClickListener = event => {
        if (!element.contains(event.target) && isVisible(
            element)) {
          element.classList.add("hidden")
          removeClickListener()
        }
      }

      const removeClickListener = () => {
        document.removeEventListener("click", outsideClickListener)
      }
      document.addEventListener("click", outsideClickListener)
    }

    const isVisible = elem => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length)

    // set token input to "?token=" query parameter
    const urlParams = new URLSearchParams(window.location.search)
    document.getElementById("token").value = urlParams.get("token")

    // set "?lang=" parameter to user lang
    const userLang = navigator.language || navigator.userLanguage
    if (!urlParams.has("lang")) { 
      urlParams.append("lang", userLang)
      window.history.replaceState({}, '', `${location.pathname}?${urlParams}`);
    }

    // html5 validators
    var username = document.getElementById("username")
    var password = document.getElementById("password")
    var confirm_password = document.getElementById("confirm_password")
    var token = document.getElementById("token")

    username.addEventListener("input", function (event) {
      if (username.validity.patternMismatch) {
        username.setCustomValidity("{{ translations.username_format }}")
      } else {
        var uname = username.value.replace(/^@/,'').split(":")[0]
        {% for each in uname_regex if each %}
          if (uname.match(/{{ each }}/)==null) {
            username.setCustomValidity("{{ translations.username_error }}")
            return
          }
        {% endfor %}
        {% for each in uname_regex_inv if each %}
          if (uname.match(/{{ each }}/)!=null) {
            username.setCustomValidity("{{ translations.username_error }}")
            return
          }
        {% endfor %}
        username.setCustomValidity("")
      }
    })

    token.addEventListener("input", function (event) {
      if (token.validity.patternMismatch) {
        token.setCustomValidity("{{ translations.case_sensitive }}")
      } else {
        token.setCustomValidity("")
      }
    })

    password.addEventListener("input", function (event) {
      if (password.validity.tooShort) {
        password.setCustomValidity('{{ translations.password_too_short }}')
      } else {
        password.setCustomValidity("")
      }
    })

    function validatePassword() {
      if (password.value != confirm_password.value) {
        confirm_password.setCustomValidity("{{ translations.password_do_not_match }}")
      } else {
        confirm_password.setCustomValidity("")
      }
    }

    password.onchange = validatePassword
    confirm_password.onkeyup = validatePassword

    function showError(message, dialog) {
      document.getElementById("error_message").innerHTML = message
      document.getElementById("error_dialog").innerHTML = dialog
      let error = document.getElementById("error")
      error.classList.remove("hidden")
      hideOnClickOutside(error)
    }

    // hijack the submit button to display the json response in a neat modal
    var form = document.getElementById("registration")

    function sendData() {
      let XHR = new XMLHttpRequest()

      // Bind the FormData object and the form element
      let FD = new FormData(form)

      // Define what happens on successful data submission
      XHR.addEventListener("load", function (event) {
        console.log(XHR.responseText)
        let response = JSON.parse(XHR.responseText)
        try {
          console.log(response)
        } catch (e) {
          if (e instanceof SyntaxError) {
            showError("{{ translations.internal_error }}", "{{ translations.contact }}")
            return
          }
        }
        if ("errcode" in response) {
          if (response["errcode"] == "MR_BAD_USER_REQUEST") {
            if ("token" in response["error"]) {
              showError(" {{ translations.token_error }} ", response["error"]["token"][0])
            } else if ("password" in response["error"]) {
              showError("{{ translations.password_error }}", response["error"]["password"][0])
            } else if ("username" in response["error"]) {
              showError("{{ translations.username_error }}", response["error"]["username"][0])
            }
            return
          } else {
            showError("{{ translations.homeserver_error }}", response["error"])
          }
        } else {
          document.getElementById("welcome").innerHTML = "{{ translations.welcome }} "  + response['user_id']
          document.getElementById("success").classList.remove("hidden")
        }

      })

      // Define what happens in case of error
      XHR.addEventListener("error", function (event) {
        showError("{{ translations.internal_error }}", "{{ translations.contact }}")
      })

      // Set up our request
      XHR.open("POST", "{{ base_url }}/register")

      // The data sent is what the user provided in the form
      XHR.send(FD)
    }

    // take over its submit event.
    form.addEventListener("submit", function (event) {
      event.preventDefault()

      sendData()
    })
  </script>
</body>

</html>


================================================
FILE: matrix_registration/tokens.py
================================================
# Standard library imports...
from datetime import datetime
import logging
import random

from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import (
    exc,
    Table,
    Column,
    Integer,
    String,
    Boolean,
    DateTime,
    ForeignKey,
)
from sqlalchemy.orm import relationship

# Local imports...
from .constants import WORD_LIST_PATH


logger = logging.getLogger(__name__)

db = SQLAlchemy()
session = db.session


def random_readable_string(length=3, wordlist=WORD_LIST_PATH):
    with open(wordlist) as f:
        lines = f.read().splitlines()
        string = ""
        for _ in range(length):
            string += random.choice(lines).title()
    return string


association_table = Table(
    "association",
    db.Model.metadata,
    Column("ips", Integer, ForeignKey("ips.id"), primary_key=True),
    Column("tokens", String(255), ForeignKey("tokens.name"), primary_key=True),
)


class IP(db.Model):
    __tablename__ = "ips"
    id = Column(Integer, primary_key=True)
    address = Column(String(255))

    def __repr__(self):
        return self.address


class Token(db.Model):
    __tablename__ = "tokens"
    name = Column(String(255), primary_key=True)
    expiration_date = Column(DateTime, nullable=True)
    max_usage = Column(Integer, default=1)
    used = Column(Integer, default=0)
    disabled = Column(Boolean, default=False)
    ips = relationship(
        "IP",
        secondary=association_table,
        lazy="subquery",
        backref=db.backref("pages", lazy=True),
    )

    def __init__(self, **kwargs):
        super(Token, self).__init__(**kwargs)
        if not self.name:
            self.name = random_readable_string()
        if not self.used:
            self.used = 0
        if not self.max_usage:
            self.max_usage = 0

    def __repr__(self):
        return self.name

    def toDict(self):
        _token = {
            "name": self.name,
            "used": self.used,
            "expiration_date": str(self.expiration_date)
            if self.expiration_date
            else None,
            "max_usage": self.max_usage,
            "ips": list(map(lambda x: x.address, self.ips)),
            "disabled": bool(self.disabled),
            "active": self.active(),
        }
        return _token

    def active(self):
        expired = False
        if self.expiration_date:
            expired = self.expiration_date < datetime.now()
        used = self.max_usage != 0 and self.max_usage <= self.used

        return (not expired) and (not used) and (not self.disabled)

    def use(self, ip_address=False):
        if self.active():
            self.used += 1
            if ip_address:
                self.ips.append(IP(address=ip_address))
            return True
        return False

    def disable(self):
        if not self.disabled:
            self.disabled = True
            return True
        return False


class Tokens:
    def __init__(self):
        self.tokens = {}

        self.load()

    def __repr__(self):
        result = ""
        for tokens_key in self.tokens:
            result += "%s, " % tokens_key
        return result[:-2]

    def toList(self):
        _tokens = []
        for tokens_key in self.tokens:
            _tokens.append(self.tokens[tokens_key].toDict())
        return _tokens

    def load(self):
        logger.debug("loading tokens from ..")
        self.tokens = {}
        for token in Token.query.all():
            logger.debug(token)
            self.tokens[token.name] = token

        logger.debug("token loaded!")

    def get_token(self, token_name):
        logger.debug("getting token by name: %s" % token_name)
        try:
            token = Token.query.filter_by(name=token_name).first()
        except KeyError:
            return False
        return token

    def active(self, token_name):
        logger.debug('checking if "%s" is active' % token_name)
        token = self.get_token(token_name)
        if token:
            return token.active()
        return False

    def use(self, token_name, ip_address=False):
        logger.debug("using token: %s" % token_name)
        token = self.get_token(token_name)
        if token:
            if token.use(ip_address):
                session.commit()
                return True
        return False

    def update(self, token_name, data):
        logger.debug("updating token: %s" % token_name)
        token = self.get_token(token_name)
        if not token:
            return False
        if "expiration_date" in data:
            token.expiration_date = data["expiration_date"]
        if "max_usage" in data:
            token.max_usage = data["max_usage"]
        if "used" in data:
            token.used = data["used"]
        if "disabled" in data:
            token.disabled = data["disabled"]
        session.commit()
        return True

    def disable(self, token_name):
        logger.debug("disabling token: %s" % token_name)
        token = self.get_token(token_name)
        if token:
            if token.disable():
                session.commit()
                return True
        return False

    def delete(self, token_name):
        logger.debug("disabling token: %s" % token_name)
        try:
            Token.query.filter_by(name=token_name).delete()
            session.commit()
        except exc.SQLAlchemyError as e:
            logger.exception(e)
            return False
        return True

    def new(self, expiration_date=None, max_usage=False):
        logger.debug(
            (
                "creating new token, with options: max_usage: {},"
                + "expiration_dates: {}"
            ).format(max_usage, expiration_date)
        )
        token = Token(expiration_date=expiration_date, max_usage=max_usage)
        self.tokens[token.name] = token
        session.add(token)
        session.commit()

        return token


tokens = None


================================================
FILE: matrix_registration/translation.py
================================================
import os
import re

import yaml

from .constants import __location__


replace_pattern = re.compile(r"{{\s*(?P<name>.[a-zA-Z_\-]+)\s*}}")


def get_translations(lang="en", replacements={}):
    default = _get_translations(replacements=replacements)
    try:
        selected = _get_translations(lang=lang, replacements=replacements)
        return {**default, **selected}
    except IOError:
        return default


def _get_translations(lang="en", replacements={}):
    path = os.path.join(__location__, f"translations/messages.{lang}.yaml")

    with open(path, "r") as stream:
        translations = yaml.load(stream, Loader=yaml.SafeLoader)

    interpolated_translations = {}
    for key, value in translations["weblate"].items():
        match = re.search(replace_pattern, value)
        while match:
            value = value.replace(
                match.group(0), str(replacements[match.group("name")])
            )
            match = re.search(replace_pattern, value)

        interpolated_translations[key] = value

    return interpolated_translations


================================================
FILE: matrix_registration/translations/messages.de.yaml
================================================
weblate:
  server_registration: "{{ server_name }} Registrierung"
  register_account: "Registriere einen Account auf {{ server_name }}"
  requires_token: "Die Registrierung erfordert ein geheimes Token"
  requires_username_and_password: |
    Für die Registrierung ist keine E-Mail erforderlich, nur ein Benutzername und ein Passwort, welches länger als {{ pw_length }} Zeichen ist.
  username: "Nutzername"
  password: "Passwort"
  confirm: "Bestätige"
  token: "Token"
  register: "registriere"
  click_to_login: "Klicke hier um einzuloggen:"
  choose_client: "oder wähle einen der vielen anderen Clienten hier:"
  username_format: "Format: @username:{{ server_name }}"
  case_sensitive: "Groß- und Kleinschreibung beachten, z.B.: SardineImpactReport"
  password_too_short: "mindestens {{ pw_length }} Zeichen lang"
  password_do_not_match: "Passwörter stimmen nicht überein"
  error: "Fehler"
  error_long: "Es gab einen Fehler währrend du registriert wurdest."
  internal_error: "Interner Server Fehler!"
  contact: "Bitte kontaktieren sie Ihren Server Administrator über dies."
  token_error: Token Fehler"
  password_error: "Password Fehler"
  username_error: "Nutzername Fehler"
  homeserver_error: "Homeserver Fehler"
  welcome: "Willkommen"


================================================
FILE: matrix_registration/translations/messages.en.yaml
================================================
weblate:
  server_registration: "{{ server_name }} registration"
  register_account: "register an account on {{ server_name }}"
  requires_token: "the registration requires a secret token"
  requires_username_and_password: >
    registration does not require an email, just a username and a password
    that's longer than {{ pw_length }} characters.
  username: "Username"
  password: "Password"
  confirm: "Confirm"
  token: "Token"
  register: "register"
  click_to_login: "Click here to login in:"
  choose_client: "or choose one of the many other clients here:"
  username_format: "format: @username:{{ server_name }}"
  case_sensitive: "case-sensitive, e.g: SardineImpactReport"
  password_too_short: "atleast {{ pw_length }} characters long"
  password_do_not_match: passwords don't match
  error: "Error"
  error_long: "There was an error while trying to register you."
  internal_error: "Internal Server Error!"
  contact: "Please contact the server admin about this."
  token_error: "Token Error"
  password_error: "Password Error"
  username_error: "Username Error"
  homeserver_error: "Homeserver Error"
  welcome: "Welcome"


================================================
FILE: matrix_registration/translations/messages.pt_BR.yaml
================================================
weblate:
  error_long: Ocorreu um erro enquanto tentávamos fazer seu registro.
  register: registrar
  confirm: Confirmar a senha
  internal_error: Erro Interno no Servidor!
  welcome: Bem-vindo
  homeserver_error: Erro no Homeserver
  username_error: Erro no Nome do Usuário
  password_error: Erro na Senha
  token_error: Erro no Token
  contact: Por favor, contacte o administrador do servidor sobre esse assunto.
  error: Erro
  password_do_not_match: as senhas não são iguais
  password_too_short: pelo menos {{ pw_length }} caracteres de comprimento
  case_sensitive: 'maiúsculas e minúsculas, p.ex.: SardinhaImpactoRelatorio'
  username_format: 'formato: @nome_do_usuário:{{ server_name }}'
  choose_client: 'ou escolha algum dos outros clientes aqui:'
  click_to_login: 'Clique aqui para logar em:'
  token: Token
  password: Senha
  username: Nome do usuário
  requires_username_and_password: "o registro não requer endereço de email, apenas\
    \ um nome de usuário e uma senha maior que {{ pw_length }} caracteres.\n"
  requires_token: o registro necessita de um token secreto
  register_account: registre uma conta no {{ server_name }}
  server_registration: Registro no {{ server_name }}


================================================
FILE: matrix_registration/translations/messages.sv.yaml
================================================
weblate:
  welcome: Välkommen
  homeserver_error: Hemserverfel
  username_error: Användarnamnsfel
  password_error: Lösenordsfel
  token_error: Token-fel
  contact: Vänligen kontakta serveradministratören angående detta.
  internal_error: Internt Serverfel!
  error_long: Det uppstod ett fel när du skulle registreras.
  error: Error
  password_do_not_match: lösenorden matchar inte
  password_too_short: åtminstone {{ pw_length }} symboler långt
  case_sensitive: 'skiftlägeskänslighet, t.ex.: SardineImpactReport'
  username_format: 'format: @användarnamn:{{ server_name }}'
  choose_client: 'eller välj en av de många andra klienterna här:'
  click_to_login: 'Klicka här för att logga in:'
  register: registrera
  token: Token
  confirm: Bekräfta
  password: Lösenord
  username: Användarnamn
  requires_username_and_password: "registreringen kräver inte en mejladress, enbart\
    \ ett användarnamn och ett lösenord som är längre än {{ pw_length }} tecken.\n"
  requires_token: registreringen kräver en hemlig token
  server_registration: '{{ server_name }} registrering'
  register_account: registrera ett konto på {{ server_name }}


================================================
FILE: matrix_registration/translations/messages.zh_Hans.yaml
================================================
weblate:
  requires_token: 注册需要一个秘密令牌
  welcome: 欢迎
  homeserver_error: 主服务器错误
  username_error: 用户名错误
  password_error: 密码错误
  token_error: 令牌错误
  contact: 请就此事与服务器管理员联系。
  internal_error: 服务器内部错误!
  error_long: 在为你注册时出现了错误。
  error: 错误
  password_do_not_match: 密码不匹配
  password_too_short: 至少 {{ pw_length }} 个字符
  case_sensitive: 大小写敏感,如 SardineImpactReport
  username_format: 格式:@用户名:{{ server_name }}
  choose_client: 或从这些其他客户端中选择:
  click_to_login: 点此登陆:
  register: 注册
  token: 令牌
  confirm: 确认密码
  password: 密码
  username: 用户名
  requires_username_and_password: "注册不需要电子邮箱,仅需用户名与长于{{ pw_length }}个字符的密码。\n"
  register_account: 在 {{ server_name }} 注册账户
  server_registration: '{{ server_name }} 注册'


================================================
FILE: matrix_registration/wordlist.txt
================================================
acrobat
africa
alaska
albert
albino
album
alcohol
alex
alpha
amadeus
amanda
amazon
america
analog
animal
antenna
antonio
apollo
april
aroma
artist
aspirin
athlete
atlas
banana
bandit
banjo
bikini
bingo
bonus
camera
canada
carbon
casino
catalog
cinema
citizen
cobra
comet
compact
complex
context
credit
critic
crystal
culture
david
delta
dialog
diploma
doctor
domino
dragon
drama
extra
fabric
final
focus
forum
galaxy
gallery
global
harmony
hotel
humor
index
japan
kilo
lemon
liter
lotus
mango
melon
menu
meter
metro
mineral
model
music
object
piano
pirate
plastic
radio
report
signal
sport
studio
subject
super
tango
taxi
tempo
tennis
textile
tokyo
total
tourist
video
visa
academy
alfred
atlanta
atomic
barbara
bazaar
brother
budget
cabaret
cadet
candle
capsule
caviar
channel
chapter
circle
cobalt
comrade
condor
crimson
cyclone
darwin
declare
denver
desert
divide
dolby
domain
double
eagle
echo
eclipse
editor
educate
edward
effect
electra
emerald
emotion
empire
eternal
evening
exhibit
expand
explore
extreme
ferrari
forget
freedom
friday
fuji
galileo
genesis
gravity
habitat
hamlet
harlem
helium
holiday
hunter
ibiza
iceberg
imagine
infant
isotope
jackson
jamaica
jasmine
java
jessica
kitchen
lazarus
letter
license
lithium
loyal
lucky
magenta
manual
marble
maxwell
mayor
monarch
monday
money
morning
mother
mystery
native
nectar
nelson
network
nikita
nobel
nobody
nominal
norway
nothing
number
october
office
oliver
opinion
option
order
outside
package
pandora
panther
papa
pattern
pedro
pencil
people
phantom
philips
pioneer
pluto
podium
portal
potato
process
proxy
pupil
python
quality
quarter
quiet
rabbit
radical
radius
rainbow
ramirez
ravioli
raymond
respect
respond
result
resume
richard
river
roger
roman
rondo
sabrina
salary
salsa
sample
samuel
saturn
savage
scarlet
scorpio
sector
serpent
shampoo
sharon
silence
simple
society
sonar
sonata
soprano
sparta
spider
sponsor
abraham
action
active
actor
adam
address
admiral
adrian
agenda
agent
airline
airport
alabama
aladdin
alarm
algebra
alibi
alice
alien
almond
alpine
amber
amigo
ammonia
analyze
anatomy
angel
annual
answer
apple
archive
arctic
arena
arizona
armada
arnold
arsenal
arthur
asia
aspect
athena
audio
august
austria
avenue
average
axiom
aztec
bagel
baker
balance
ballad
ballet
bambino
bamboo
baron
basic
basket
battery
belgium
benefit
berlin
bermuda
bernard
bicycle
binary
biology
bishop
blitz
block
blonde
bonjour
boris
boston
bottle
boxer
brandy
bravo
brazil
bridge
british
bronze
brown
bruce
bruno
brush
burger
burma
cabinet
cactus
cafe
cairo
calypso
camel
campus
canal
cannon
canoe
cantina
canvas
canyon
capital
caramel
caravan
career
cargo
carlo
carol
carpet
cartel
cartoon
castle
castro
cecilia
cement
center
century
ceramic
chamber
chance
change
chaos
charlie
charm
charter
cheese
chef
chemist
cherry
chess
chicago
chicken
chief
china
cigar
circus
city
clara
classic
claudia
clean
client
climax
clinic
clock
club
cockpit
coconut
cola
collect
colombo
colony
color
combat
comedy
command
company
concert
connect
consul
contact
contour
control
convert
copy
corner
corona
correct
cosmos
couple
courage
cowboy
craft
crash
cricket
crown
cuba
dallas
dance
daniel
decade
decimal
degree
delete
deliver
delphi
deluxe
demand
demo
denmark
derby
design
detect
develop
diagram
diamond
diana
diego
diesel
diet
digital
dilemma
direct
disco
disney
distant
dollar
dolphin
donald
drink
driver
dublin
duet
dynamic
earth
east
ecology
economy
edgar
egypt
elastic
elegant
element
elite
elvis
email
empty
energy
engine
english
episode
equator
escape
escort
ethnic
europe
everest
evident
exact
example
exit
exotic
export
express
factor
falcon
family
fantasy
fashion
fiber
fiction
fidel
fiesta
figure
film
filter
finance
finish
finland
first
flag
flash
florida
flower
fluid
flute
folio
ford
forest
formal
formula
fortune
forward
fragile
france
frank
fresh
friend
frozen
future
gabriel
gamma
garage
garcia
garden
garlic
gemini
general
genetic
genius
germany
gloria
gold
golf
gondola
gong
good
gordon
gorilla
grand
granite
graph
green
group
guide
guitar
guru
hand
happy
harbor
harvard
havana
hawaii
helena
hello
henry
hilton
history
horizon
house
human
icon
idea
igloo
igor
image
impact
import
india
indigo
input
insect
instant
iris
italian
jacket
jacob
jaguar
janet
jargon
jazz
jeep
john
joker
jordan
judo
jumbo
june
jungle
junior
jupiter
karate
karma
kayak
kermit
king
koala
korea
labor
lady
lagoon
laptop
laser
latin
lava
lecture
left
legal
level
lexicon
liberal
libra
lily
limbo
limit
linda
linear
lion
liquid
little
llama
lobby
lobster
local
logic
logo
lola
london
lucas
lunar
machine
macro
madam
madonna
madrid
maestro
magic
magnet
magnum
mailbox
major
mama
mambo
manager
manila
marco
marina
market
mars
martin
marvin
mary
master
matrix
maximum
media
medical
mega
melody
memo
mental
mentor
mercury
message
metal
meteor
method
mexico
miami
micro
milk
million
minimum
minus
minute
miracle
mirage
miranda
mister
mixer
mobile
modem
modern
modular
moment
monaco
monica
monitor
mono
monster
montana
morgan
motel
motif
motor
mozart
multi
museum
mustang
natural
neon
nepal
neptune
nerve
neutral
nevada
news
next
ninja
nirvana
normal
nova
novel
nuclear
numeric
nylon
oasis
observe
ocean
octopus
olivia
olympic
omega
opera
optic
optimal
orange
orbit
organic
orient
origin
orlando
oscar
oxford
oxygen
ozone
pablo
pacific
pagoda
palace
pamela
panama
pancake
panda
panel
panic
paradox
pardon
paris
parker
parking
parody
partner
passage
passive
pasta
pastel
patent
patient
patriot
patrol
pegasus
pelican
penguin
pepper
percent
perfect
perfume
period
permit
person
peru
phone
photo
picasso
picnic
picture
pigment
pilgrim
pilot
pixel
pizza
planet
plasma
plaza
pocket
poem
poetic
poker
polaris
police
politic
polo
polygon
pony
popcorn
popular
postage
precise
prefix
premium
present
price
prince
printer
prism
private
prize
product
profile
program
project
protect
proton
public
pulse
puma
pump
pyramid
queen
radar
ralph
random
rapid
rebel
record
recycle
reflex
reform
regard
regular
relax
reptile
reverse
ricardo
right
ringo
risk
ritual
robert
robot
rocket
rodeo
romeo
royal
russian
safari
salad
salami
salmon
salon
salute
samba
sandra
santana
sardine
school
scoop
scratch
screen
script
scroll
second
secret
section
segment
select
seminar
senator
senior
sensor
serial
service
shadow
sharp
sheriff
shock
short
shrink
sierra
silicon
silk
silver
similar
simon
single
siren
slang
slogan
smart
smoke
snake
social
soda
solar
solid
solo
sonic
source
soviet
special
speed
sphere
spiral
spirit
spring
static
status
stereo
stone
stop
street
strong
student
style
sultan
susan
sushi
suzuki
switch
symbol
system
tactic
tahiti
talent
tarzan
telex
texas
theory
thermos
tiger
titanic
tomato
topic
tornado
toronto
torpedo
totem
tractor
traffic
transit
trapeze
travel
tribal
trick
trident
trilogy
tripod
tropic
trumpet
tulip
tuna
turbo
twist
ultra
uniform
union
uranium
vacuum
valid
vampire
vanilla
vatican
velvet
ventura
venus
vertigo
veteran
victor
vienna
viking
village
vincent
violet
violin
virtual
virus
vision
visitor
visual
vitamin
viva
vocal
vodka
volcano
voltage
volume
voyage
water
weekend
welcome
western
window
winter
wizard
wolf
world
xray
yankee
yoga
yogurt
yoyo
zebra
zero
zigzag
zipper
zodiac
zoom
acid
adios
agatha
alamo
alert
almanac
aloha
andrea
anita
arcade
aurora
avalon
baby
baggage
balloon
bank
basil
begin
biscuit
blue
bombay
botanic
brain
brenda
brigade
cable
calibre
carmen
cello
celtic
chariot
chrome
citrus
civil
cloud
combine
common
cool
copper
coral
crater
cubic
cupid
cycle
depend
door
dream
dynasty
edison
edition
enigma
equal
eric
event
evita
exodus
extend
famous
farmer
food
fossil
frog
fruit
geneva
gentle
george
giant
gilbert
gossip
gram
greek
grille
hammer
harvest
hazard
heaven
herbert
heroic
hexagon
husband
immune
inca
inch
initial
isabel
ivory
jason
jerome
joel
joshua
journal
judge
juliet
jump
justice
kimono
kinetic
leonid
leopard
lima
maze
medusa
member
memphis
michael
miguel
milan
mile
miller
mimic
mimosa
mission
monkey
moral
moses
mouse
nancy
natasha
nebula
nickel
nina
noise
orchid
oregano
origami
orinoco
orion
othello
paper
paprika
prelude
prepare
pretend
promise
prosper
provide
puzzle
remote
repair
reply
rival
riviera
robin
rose
rover
rudolf
saga
sahara
scholar
shelter
ship
shoe
sigma
sister
sleep
smile
spain
spark
split
spray
square
stadium
star
storm
story
strange
stretch
stuart
subway
sugar
sulfur
summer
survive
sweet
swim
table
taboo
target
teacher
telecom
temple
tibet
ticket
tina
today
toga
tommy
tower
trivial
tunnel
turtle
twin
uncle
unicorn
unique
update
valery
vega
version
voodoo
warning
william
wonder
year
yellow
young
absent
absorb
absurd
accent
alfonso
alias
ambient
anagram
andy
anvil
appear
apropos
archer
ariel
armor
arrow
austin
avatar
axis
baboon
bahama
bali
balsa
barcode
bazooka
beach
beast
beatles
beauty
before
benny
betty
between
beyond
billy
bison
blast
bless
bogart
bonanza
book
border
brave
bread
break
broken
bucket
buenos
buffalo
bundle
button
buzzer
byte
caesar
camilla
canary
candid
carrot
cave
chant
child
choice
chris
cipher
clarion
clark
clever
cliff
clone
conan
conduct
congo
costume
cotton
cover
crack
current
danube
data
decide
deposit
desire
detail
dexter
dinner
donor
druid
drum
easy
eddie
enjoy
enrico
epoxy
erosion
except
exile
explain
fame
fast
father
felix
field
fiona
fire
fish
flame
flex
flipper
float
flood
floor
forbid
forever
fractal
frame
freddie
front
fuel
gallop
game
garbo
gate
gelatin
gibson
ginger
giraffe
gizmo
glass
goblin
gopher
grace
gray
gregory
grid
griffin
ground
guest
gustav
gyro
hair
halt
harris
heart
heavy
herman
hippie
hobby
honey
hope
horse
hostel
hydro
imitate
info
ingrid
inside
invent
invest
invite
ivan
james
jester
jimmy
join
joseph
juice
julius
july
kansas
karl
kevin
kiwi
ladder
lake
laura
learn
legacy
legend
lesson
life
light
list
locate
lopez
lorenzo
love
lunch
malta
mammal
margin
margo
marion
mask
match
mayday
meaning
mercy
middle
mike
mirror
modest
morph
morris
mystic
nadia
nato
navy
needle
neuron
never
newton
nice
night
nissan
nitro
nixon
north
oberon
octavia
ohio
olga
open
opus
orca
oval
owner
page
paint
palma
parent
parlor
parole
paul
peace
pearl
perform
phoenix
phrase
pierre
pinball
place
plate
plato
plume
pogo
point
polka
poncho
powder
prague
press
presto
pretty
prime
promo
quest
quick
quiz
quota
race
rachel
raja
ranger
region
remark
rent
reward
rhino
ribbon
rider
road
rodent
round
rubber
ruby
rufus
sabine
saddle
sailor
saint
salt
scale
scuba
season
secure
shake
shallow
shannon
shave
shelf
sherman
shine
shirt
side
sinatra
sincere
size
slalom
slow
small
snow
sofia
song
sound
south
speech
spell
spend
spoon
stage
stamp
stand
state
stella
stick
sting
stock
store
sunday
sunset
support
supreme
sweden
swing
tape
tavern
think
thomas
tictac
time
toast
tobacco
tonight
torch
torso
touch
toyota
trade
tribune
trinity
triton
truck
trust
type
under
unit
urban
urgent
user
value
vendor
venice
verona
vibrate
virgo
visible
vista
vital
voice
vortex
waiter
watch
wave
weather
wedding
wheel
whiskey
wisdom
android
annex
armani
cake
confide
deal
define
dispute
genuine
idiom
impress
include
ironic
null
nurse
obscure
prefer
prodigy
ego
fax
jet
job
rio
ski
yes


================================================
FILE: resources/docker-run.sh
================================================
#!/bin/sh

docker run \
  -it --rm \
  --user "$(id -u):$(id -g)" \
  --volume $(pwd)/data:/data  \
  matrix-registration:latest  \
  "$@"


================================================
FILE: resources/docker-serve.sh
================================================
#!/bin/sh

docker run \
  -d \
  --user "$(id -u):$(id -g)" \
  --network matrix \
  --publish 5000:5000/tcp \
  --volume $(pwd)/data:/data \
  matrix-registration:latest


================================================
FILE: resources/example.html
================================================
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Matrix Registration</title>
  <style>
    input:invalid {
      border: 2px dashed red;
    }

    input:valid {
      border: 2px solid black;
    }
    input {
      width: 100%;
    }
  </style>
</head>

<body>
  <section style="display:flex;justify-content:center;align-items:center;">
    <!-- make sure to adjust port here -->
    <form action="localhost:5000/register" method="post">
      <label for="username"> Enter your username:</label><br>
      <input id="username" name="username" type="text"
      required pattern="^@?[a-zA-Z_\-=\.\/0-9]+(:matrix\.org)?$" 
      required minlength="1" maxlength="200">
      <!-- change to your homeserver -->
      <br>
      <label for="password">Enter your password:</label><br>
      <input id="password" name="password" type="password"
      required minlength="8" maxlength="128">
      <br>
      <label for="confirm_password">Repeat your password:</label><br>
      <input id="confirm_password" name="confirm" type="password"
      required>
      <br>
      <label for="token">Enter your invite token:</label><br>
      <input id="token" name="token" type="text"
      required pattern="^([A-Z][a-z]+)+$">
      <br><br>
      <input type="submit" value="register">
    </form>
  </section>
  <script>
    // all javascript here is optional, the registration form works fine without

    // see https://stackoverflow.com/a/901144/3779427
    function getParameterByName(name, url) {
        if (!url) url = window.location.href;
        name = name.replace(/[\[\]]/g, "\\$&");
        var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
            results = regex.exec(url);
        if (!results) return null;
        if (!results[2]) return '';
        return decodeURIComponent(results[2].replace(/\+/g, " "));
    }

    // set token input to "?token=" query parameter
    document.getElementById('token').value = getParameterByName("token");

    // html5 validators
    var username = document.getElementById("username");
    var password = document.getElementById("password");
    var confirm_password = document.getElementById("confirm_password");
    var token = document.getElementById("token");

    username.addEventListener("input", function (event) {
      if (username.validity.typeMismatch) {
        username.setCustomValidity("format: @username:matrix.org");
      } else {
        username.setCustomValidity("");
      }
    });

    token.addEventListener("input", function (event) {
      if (token.validity.typeMismatch) {
        token.setCustomValidity("case-sensitive, e.g: SardineImpactReport");
      } else {
        token.setCustomValidity("");
      }
    });

    password.addEventListener("input", function (event) {
      if (password.validity.typeMismatch) {
        password.setCustomValidity("atleast 8 characters long");
      } else {
        password.setCustomValidity("");
      }
    });

    function validatePassword(){
      if(password.value != confirm_password.value) {
        confirm_password.setCustomValidity("passwords don't match");
      } else {
        confirm_password.setCustomValidity("");
      }
    }

    password.onchange = validatePassword;
    confirm_password.onkeyup = validatePassword;
  </script>
</body>
</html>


================================================
FILE: setup.py
================================================
#!/usr/bin/env python
import codecs
import os
import re
import setuptools
import glob

here = os.path.abspath(os.path.dirname(__file__))


def read(*parts):
    with codecs.open(os.path.join(here, *parts), 'r') as fp:
        return fp.read()


def find_version(*file_paths):
    version_file = read(*file_paths)
    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                              version_file, re.M)
    if version_match:
        return version_match.group(1)
    raise RuntimeError("Unable to find version string.")


test_requirements = [
        "parameterized>=0.7.0"
]


setuptools.setup(
    name='matrix-registration',
    version=find_version("matrix_registration", "__init__.py"),
    description='token based matrix registration app',
    author='Jona Abdinghoff (ZerataX)',
    author_email='mail@zera.tax',
    long_description=open("README.md").read(),
    long_description_content_type="text/markdown",
    url="https://github.com/zeratax/matrix-registration",
    packages=['matrix_registration'],
    package_data={'matrix_registration': ['*.txt','*.json',
                                          'translations/*.yaml',
                                          'templates/*.html',
                                          'static/css/*.css',
                                          'static/fonts/*.woff2',
                                          'static/images/*.jpg',
                                          'static/images/*.png',
                                          'static/images/*.ico']},
    python_requires='~=3.7',
    install_requires=[
        "alembic>=1.8",
        "appdirs>=1.4.4",
        "Flask>=2.2",
        "Flask-SQLAlchemy>=2.5.1",
        "flask-cors>=3.0.10",
        "flask-httpauth>=4.7.0",
        "flask-limiter>=2.6",
        "PyYAML>=6.0",
        "jsonschema>=4.17",
        "requests>=2.28",
        "SQLAlchemy>=1.4",
        "waitress>=2.1",
        "WTForms>=3.0"
    ],
    tests_require=test_requirements,
    extras_require={
        "postgres":  ["psycopg2-binary>=2.8.4"],
        "testing": test_requirements
    },
    classifiers=[
        "Development Status :: 4 - Beta",
        "Topic :: Communications :: Chat",
        "License :: OSI Approved :: MIT License",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9"
    ],
    entry_points={
        'console_scripts': [
            'matrix-registration=matrix_registration.app:cli'
        ],
    },
    data_files=[
        ("config", ["config.sample.yaml"]),
        (".", ["alembic.ini"]),
        ("alembic", ["alembic/env.py"]),
        ("alembic/versions", glob.glob("alembic/versions/*.py"))
    ]
)


================================================
FILE: shell.nix
================================================
{ pkgs ? import <nixpkgs> { } }:

(let matrix-registration = pkgs.callPackage ./default.nix { inherit pkgs; };
in pkgs.python3.withPackages
(ps: [ matrix-registration pkgs.black ps.alembic ps.parameterized ps.flake8 ])).env


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/context.py
================================================
import os
import sys

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

import matrix_registration


================================================
FILE: tests/localhost.log.config
================================================
version: 1
formatters:
  precise:
    format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    formatter: precise
loggers:
    synapse.storage.SQL:
        level: INFO
root:
    level: INFO
    handlers: [console]
disable_existing_loggers: false


================================================
FILE: tests/localhost.signing.key
================================================
ed25519 a_snwG JXtOu5WvTJOETWmItITGUlnqWy6WO4Ovew2flTRYD90


================================================
FILE: tests/test_registration.py
================================================
# -*- coding: utf-8 -*-
# Standard library imports...
import hashlib
import hmac
import json
import logging.config
import os
import random
import re
import string
import sys
import unittest
from datetime import datetime
from unittest.mock import patch
from urllib.parse import urlparse

# Third-party imports...
import yaml
from parameterized import parameterized
from requests import exceptions

# Local imports...
try:
    from .context import matrix_registration
except ModuleNotFoundError:
    from context import matrix_registration
from matrix_registration.config import Config
from matrix_registration.tokens import db
from matrix_registration.app import (
    create_app,
    cli,
)

logger = logging.getLogger(__name__)

LOGGING = {
    "version": 1,
    "root": {"level": "NOTSET", "handlers": ["console"]},
    "formatters": {
        "precise": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "NOTSET",
            "formatter": "precise",
            "stream": "ext://sys.stdout",
        }
    },
}

GOOD_CONFIG = {
    "server_location": "https://matrix.org",
    "server_name": "matrix.org",
    "registration_shared_secret": "coolsharesecret",
    "admin_api_shared_secret": "coolpassword",
    "base_url": "/element",
    "client_redirect": "",
    "client_logo": "",
    "db": "sqlite:///%s/tests/db.sqlite" % (os.getcwd(),),
    "host": "",
    "port": 5000,
    "rate_limit": ["1000 per day", "100 per minute"],
    "allow_cors": False,
    "password": {"min_length": 8},
    "username": {
        "validation_regex": ["[a-z\d]"],
        "invalidation_regex": [".*?(admin|support).*?"],
    },
    "ip_logging": False,
    "logging": LOGGING,
}

BAD_CONFIG1 = dict(  # wrong matrix server location -> 500
    GOOD_CONFIG.items(),
    server_location="https://wronghs.org",
)

BAD_CONFIG2 = dict(  # wrong admin secret password -> 401
    GOOD_CONFIG.items(),
    admin_api_shared_secret="wrongpassword",
)

BAD_CONFIG3 = dict(  # wrong matrix shared password -> 500
    GOOD_CONFIG.items(),
    registration_shared_secret="wrongsecret",
)

usernames = []
nonces = []
logging.config.dictConfig(LOGGING)


def mock_new_user(username):
    access_token = "".join(
        random.choices(string.ascii_lowercase + string.digits, k=256)
    )
    device_id = "".join(random.choices(string.ascii_uppercase, k=8))
    home_server = matrix_registration.config.config.server_location
    username = username.rsplit(":")[0].split("@")[-1]
    user_id = "@{}:{}".format(username, home_server)
    usernames.append(username)

    user = {
        "access_token": access_token,
        "device_id": device_id,
        "home_server": home_server,
        "user_id": user_id,
    }
    return user


def mocked__get_nonce(server_location):
    nonce = "".join(random.choices(string.ascii_lowercase + string.digits, k=129))
    nonces.append(nonce)
    return nonce


def mocked_requests_post(*args, **kwargs):
    class MockResponse:
        def __init__(self, json_data, status_code):
            self.json_data = json_data
            self.status_code = status_code

        def json(self):
            return self.json_data

        def raise_for_status(self):
            if self.status_code == 200:
                return self.status_code
            else:
                raise exceptions.HTTPError(response=self)

    # print(args[0])
    # print(matrix_registration.config.config.server_location)
    domain = urlparse(GOOD_CONFIG["server_location"]).hostname
    re_mxid = r"^@?[a-zA-Z_\-=\.\/0-9]+(:" + re.escape(domain) + r")?$"
    location = "_synapse/admin/v1/register"

    if args[0] == "%s/%s" % (GOOD_CONFIG["server_location"], location):
        if kwargs:
            req = kwargs["json"]
            if not req["nonce"] in nonces:
                return MockResponse(
                    {"'errcode': 'M_UNKOWN", "'error': 'unrecognised nonce'"}, 400
                )

            mac = hmac.new(
                key=str.encode(GOOD_CONFIG["registration_shared_secret"]),
                digestmod=hashlib.sha1,
            )

            mac.update(req["nonce"].encode())
            mac.update(b"\x00")
            mac.update(req["username"].encode())
            mac.update(b"\x00")
            mac.update(req["password"].encode())
            mac.update(b"\x00")
            mac.update(b"admin" if req["admin"] else b"notadmin")
            mac = mac.hexdigest()
            if not re.search(re_mxid, req["username"]):
                return MockResponse(
                    {
                        "'errcode': 'M_INVALID_USERNAME",
                        "'error': 'User ID can only contain"
                        + "characters a-z, 0-9, or '=_-./'",
                    },
                    400,
                )
            if req["username"].rsplit(":")[0].split("@")[-1] in usernames:
                return MockResponse(
                    {"errcode": "M_USER_IN_USE", "error": "User ID already taken."}, 400
                )
            if req["mac"] != mac:
                return MockResponse(
                    {"errcode": "M_UNKNOWN", "error": "HMAC incorrect"}, 403
                )
            return MockResponse(mock_new_user(req["username"]), 200)
    return MockResponse(None, 404)


class TokensTest(unittest.TestCase):
    def setUp(self):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)
        app = create_app(testing=True)
        with app.app_context():
            app.config.from_mapping(
                SQLALCHEMY_DATABASE_URI=matrix_registration.config.config.db,
                SQLALCHEMY_TRACK_MODIFICATIONS=False,
            )
            db.init_app(app)
            db.create_all()

        self.app = app

    def tearDown(self):
        os.remove(matrix_registration.config.config.db[10:])

    def test_random_readable_string(self):
        for n in range(10):
            string = matrix_registration.tokens.random_readable_string(length=n)
            words = re.sub("([a-z])([A-Z])", r"\1 \2", string).split()
            self.assertEqual(len(words), n)

    def test_tokens_empty(self):
        with self.app.app_context():
            test_tokens = matrix_registration.tokens.Tokens()

            # no token should exist at this point
            self.assertFalse(test_tokens.active(""))
            test_token = test_tokens.new()

            # no empty token should have been created
            self.assertFalse(test_tokens.active(""))

    def test_tokens_disable(self):
        with self.app.app_context():
            test_tokens = matrix_registration.tokens.Tokens()
            test_token = test_tokens.new()

            # new tokens should be active first, inactive after disabling it
            self.assertTrue(test_token.active())
            self.assertTrue(test_token.disable())
            self.assertFalse(test_token.active())

            test_token2 = test_tokens.new()

            self.assertTrue(test_tokens.active(test_token2.name))
            self.assertTrue(test_tokens.disable(test_token2.name))
            self.assertFalse(test_tokens.active(test_token2.name))

            test_token3 = test_tokens.new()
            test_token3.use()

            self.assertFalse(test_tokens.active(test_token2.name))
            self.assertFalse(test_tokens.disable(test_token2.name))
            self.assertFalse(test_tokens.active(test_token2.name))

    def test_tokens_load(self):
        with self.app.app_context():
            test_tokens = matrix_registration.tokens.Tokens()

            test_token = test_tokens.new()
            test_token2 = test_tokens.new()
            test_token3 = test_tokens.new(max_usage=True)
            test_token4 = test_tokens.new(
                expiration_date=datetime.fromisoformat("2111-01-01")
            )
            test_token5 = test_tokens.new(
                expiration_date=datetime.fromisoformat("1999-01-01")
            )

            test_tokens.disable(test_token2.name)
            test_tokens.use(test_token3.name)
            test_tokens.use(test_token4.name)

            test_tokens.load()

            # token1: active, unused, no expiration date
            # token2: inactive, unused, no expiration date
            # token3: used once, one-time, now inactive
            # token4: active, used once, expiration date
            # token5: inactive, expiration date

            self.assertEqual(
                test_token.name, test_tokens.get_token(test_token.name).name
            )
            self.assertEqual(
                test_token2.name, test_tokens.get_token(test_token2.name).name
            )
            self.assertEqual(
                test_token2.active(), test_tokens.get_token(test_token2.name).active()
            )
            self.assertEqual(
                test_token3.used, test_tokens.get_token(test_token3.name).used
            )
            self.assertEqual(
                test_token3.active(), test_tokens.get_token(test_token3.name).active()
            )
            self.assertEqual(
                test_token4.used, test_tokens.get_token(test_token4.name).used
            )
            self.assertEqual(
                test_token4.expiration_date,
                test_tokens.get_token(test_token4.name).expiration_date,
            )
            self.assertEqual(
                test_token5.active(), test_tokens.get_token(test_token5.name).active()
            )

    @parameterized.expand(
        [
            [None, False],
            [datetime.fromisoformat("2100-01-12"), False],
            [None, True],
            [datetime.fromisoformat("2100-01-12"), True],
        ]
    )
    def test_tokens_new(self, expiration_date, max_usage):
        with self.app.app_context():
            test_tokens = matrix_registration.tokens.Tokens()
            test_token = test_tokens.new(
                expiration_date=expiration_date, max_usage=max_usage
            )

            self.assertIsNotNone(test_token)
            if expiration_date:
                self.assertIsNotNone(test_token.expiration_date)
            else:
                self.assertIsNone(test_token.expiration_date)
            if max_usage:
                self.assertTrue(test_token.max_usage)
            else:
                self.assertFalse(test_token.max_usage)
            self.assertTrue(test_tokens.active(test_token.name))

    @parameterized.expand(
        [
            [None, False, 10, True],
            [datetime.fromisoformat("2100-01-12"), False, 10, True],
            [None, True, 1, False],
            [None, True, 0, True],
            [datetime.fromisoformat("2100-01-12"), True, 1, False],
            [datetime.fromisoformat("2100-01-12"), True, 2, False],
            [datetime.fromisoformat("2100-01-12"), True, 0, True],
        ]
    )
    def test_tokens_active_form(self, expiration_date, max_usage, times_used, active):
        with self.app.app_context():
            test_tokens = matrix_registration.tokens.Tokens()
            test_token = test_tokens.new(
                expiration_date=expiration_date, max_usage=max_usage
            )

            for n in range(times_used):
                test_tokens.use(test_token.name)

            if not max_usage:
                self.assertEqual(test_token.used, times_used)
            elif times_used == 0:
                self.assertEqual(test_token.used, 0)
            else:
                self.assertEqual(test_token.used, 1)
            self.assertEqual(test_tokens.active(test_token.name), active)

    @parameterized.expand(
        [
            [None, True],
            [datetime.fromisoformat("2100-01-12"), False],
            [datetime.fromisoformat("2200-01-13"), True],
        ]
    )
    def test_tokens_active(self, expiration_date, active):
        with self.app.app_context():
            test_tokens = matrix_registration.tokens.Tokens()
            test_token = test_tokens.new(expiration_date=expiration_date)

            self.assertEqual(test_tokens.active(test_token.name), True)
            # date changed to after expiration date
            with patch("matrix_registration.tokens.datetime") as mock_date:
                mock_date.now.return_value = datetime.fromisoformat("2200-01-12")
                self.assertEqual(test_tokens.active(test_token.name), active)

    @parameterized.expand(
        [
            ["DoubleWizardSky"],
            ["null"],
            ["false"],
        ]
    )
    def test_tokens_repr(self, name):
        with self.app.app_context():
            test_token1 = matrix_registration.tokens.Token(name=name)

            self.assertEqual(str(test_token1), name)

    def test_token_repr(self):
        with self.app.app_context():
            test_tokens = matrix_registration.tokens.Tokens()
            test_token1 = test_tokens.new()
            test_token2 = test_tokens.new()
            test_token3 = test_tokens.new()
            test_token4 = test_tokens.new()
            test_token5 = test_tokens.new()

            expected_answer = (
                    "%s, " % test_token1.name
                    + "%s, " % test_token2.name
                    + "%s, " % test_token3.name
                    + "%s, " % test_token4.name
                    + "%s" % test_token5.name
            )

            self.assertEqual(str(test_tokens), expected_answer)


class ApiTest(unittest.TestCase):
    def setUp(self):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)
        app = create_app(testing=True)
        with app.app_context():
            app.config.from_mapping(
                SQLALCHEMY_DATABASE_URI=matrix_registration.config.config.db,
                SQLALCHEMY_TRACK_MODIFICATIONS=False,
            )
            db.init_app(app)
            db.create_all()
            self.client = app.test_client()
        self.app = app

    def tearDown(self):
        os.remove(matrix_registration.config.config.db[10:])

    @parameterized.expand(
        [
            ["test1", "test1234", "test1234", True, 200],
            ["", "test1234", "test1234", True, 400],
            ["test2", "", "test1234", True, 400],
            ["test3", "test1234", "", True, 400],
            ["test4", "test1234", "test1234", False, 400],
            ["@test5:matrix.org", "test1234", "test1234", True, 200],
            ["@test6:wronghs.org", "test1234", "test1234", True, 400],
            ["test7", "test1234", "tet1234", True, 400],
            ["teüst8", "test1234", "test1234", True, 400],
            ["@test9@matrix.org", "test1234", "test1234", True, 400],
            ["test11@matrix.org", "test1234", "test1234", True, 400],
            ["", "test1234", "test1234", True, 400],
            [
                "".join(random.choices(string.ascii_uppercase, k=256)),
                "test1234",
                "test1234",
                True,
                400,
            ],
            ["@admin:matrix.org", "test1234", "test1234", True, 400],
            ["matrixadmin123", "test1234", "test1234", True, 400],
        ]
    )
    # check form activeators
    @patch("matrix_registration.matrix_api._get_nonce", side_effect=mocked__get_nonce)
    @patch(
        "matrix_registration.matrix_api.requests.post", side_effect=mocked_requests_post
    )
    def test_register(
            self, username, password, confirm, token, status, mock_get, mock_nonce
    ):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)
        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(
                expiration_date=None, max_usage=True
            )
            # replace matrix with in config set hs
            domain = urlparse(
                matrix_registration.config.config.server_location
            ).hostname
            if username:
                username = username.replace("matrix.org", domain)

            if not token:
                test_token.name = ""
            rv = self.client.post(
                "/register",
                data=dict(
                    username=username,
                    password=password,
                    confirm=confirm,
                    token=test_token.name,
                ),
            )
            if rv.status_code == 200:
                account_data = json.loads(rv.data.decode("utf8").replace("'", '"'))
                # print(account_data)
            self.assertEqual(rv.status_code, status)

    @patch("matrix_registration.matrix_api._get_nonce", side_effect=mocked__get_nonce)
    @patch(
        "matrix_registration.matrix_api.requests.post", side_effect=mocked_requests_post
    )
    def test_register_wrong_hs(self, mock_get, mock_nonce):
        matrix_registration.config.config = Config(data=BAD_CONFIG1)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(
                expiration_date=None, max_usage=True
            )
            rv = self.client.post(
                "/register",
                data=dict(
                    username="username",
                    password="password",
                    confirm="password",
                    token=test_token.name,
                ),
            )
            self.assertEqual(rv.status_code, 500)

    @patch("matrix_registration.matrix_api._get_nonce", side_effect=mocked__get_nonce)
    @patch(
        "matrix_registration.matrix_api.requests.post", side_effect=mocked_requests_post
    )
    def test_register_wrong_secret(self, mock_get, mock_nonce):
        matrix_registration.config.config = Config(data=BAD_CONFIG3)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(
                expiration_date=None, max_usage=True
            )
            rv = self.client.post(
                "/register",
                data=dict(
                    username="username",
                    password="password",
                    confirm="password",
                    token=test_token.name,
                ),
            )
            self.assertEqual(rv.status_code, 500)

    def test_get_tokens(self):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(
                expiration_date=None, max_usage=True
            )

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.get("/api/token", headers=headers)

            self.assertEqual(rv.status_code, 200)
            token_data = json.loads(rv.data.decode("utf8").replace("'", '"'))

            self.assertEqual(token_data[0]["expiration_date"], None)
            self.assertEqual(token_data[0]["max_usage"], True)

    def test_error_get_tokens(self):
        matrix_registration.config.config = Config(data=BAD_CONFIG2)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(
                expiration_date=None, max_usage=True
            )

            secret = matrix_registration.config.config.admin_api_shared_secret
            matrix_registration.config.config = Config(data=GOOD_CONFIG)
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.get("/api/token", headers=headers)

            self.assertEqual(rv.status_code, 401)
            token_data = json.loads(rv.data.decode("utf8").replace("'", '"'))

            self.assertEqual(token_data["errcode"], "MR_BAD_SECRET")
            self.assertEqual(token_data["error"], "wrong shared secret")

    @parameterized.expand(
        [
            [None, True, None],
            ["2020-12-24", False, "2020-12-24 00:00:00"],
            ["2200-05-12", True, "2200-05-12 00:00:00"],
        ]
    )
    def test_post_token(self, expiration_date, max_usage, parsed_date):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(
                expiration_date=None, max_usage=True
            )

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.post(
                "/api/token",
                data=json.dumps(
                    dict(expiration_date=expiration_date, max_usage=max_usage)
                ),
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 200)
            token_data = json.loads(rv.data.decode("utf8").replace("'", '"'))
            self.assertEqual(token_data["expiration_date"], parsed_date)
            self.assertEqual(token_data["max_usage"], max_usage)
            self.assertTrue(token_data["name"] is not None)

    def test_error_post_token(self):
        matrix_registration.config.config = Config(data=BAD_CONFIG2)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(
                expiration_date=None, max_usage=True
            )

            secret = matrix_registration.config.config.admin_api_shared_secret
            matrix_registration.config.config = Config(data=GOOD_CONFIG)
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.post(
                "/api/token",
                data=json.dumps(dict(expiration_date="24.12.2020", max_usage=False)),
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 401)
            token_data = json.loads(rv.data.decode("utf8").replace("'", '"'))

            self.assertEqual(token_data["errcode"], "MR_BAD_SECRET")
            self.assertEqual(token_data["error"], "wrong shared secret")

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.post(
                "/api/token",
                data=json.dumps(dict(expiration_date="2020-24-12", max_usage=False)),
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 400)
            token_data = json.loads(rv.data.decode("utf8"))
            self.assertEqual(token_data["errcode"], "MR_BAD_DATE_FORMAT")
            self.assertEqual(token_data["error"], "date wasn't in YYYY-MM-DD format")

    def test_patch_token(self):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(max_usage=True)

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.patch(
                "/api/token/" + test_token.name,
                data=json.dumps(dict(disabled=True)),
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 200)
            token_data = json.loads(rv.data.decode("utf8").replace("'", '"'))
            self.assertEqual(token_data["active"], False)
            self.assertEqual(token_data["max_usage"], True)
            self.assertEqual(token_data["name"], test_token.name)

    def test_error_patch_token(self):
        matrix_registration.config.config = Config(data=BAD_CONFIG2)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(max_usage=True)

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            matrix_registration.config.config = Config(data=GOOD_CONFIG)
            rv = self.client.patch(
                "/api/token/" + test_token.name,
                data=json.dumps(dict(disabled=True)),
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 401)
            token_data = json.loads(rv.data.decode("utf8").replace("'", '"'))
            self.assertEqual(token_data["errcode"], "MR_BAD_SECRET")
            self.assertEqual(token_data["error"], "wrong shared secret")

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.patch(
                "/api/token/" + test_token.name,
                data=json.dumps(dict(active=False)),
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 400)
            token_data = json.loads(rv.data.decode("utf8"))
            self.assertEqual(token_data["errcode"], "MR_BAD_USER_REQUEST")
            self.assertEqual(
                token_data["error"], "you're not allowed to change this property"
            )

            rv = self.client.patch(
                "/api/token/" + "nicememe",
                data=json.dumps(dict(disabled=True)),
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 404)
            token_data = json.loads(rv.data.decode("utf8"))
            self.assertEqual(token_data["errcode"], "MR_TOKEN_NOT_FOUND")
            self.assertEqual(token_data["error"], "token does not exist")

    def test_delete_token(self):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(max_usage=True)

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.get(
                "/api/token/" + test_token.name,
                content_type="application/json",
                headers=headers,
            )
            self.assertEqual(rv.status_code, 200)

            rv = self.client.delete(
                "/api/token/" + test_token.name,
                content_type="application/json",
                headers=headers,
            )
            self.assertEqual(rv.status_code, 200)

            rv = self.client.get(
                "/api/token/" + test_token.name,
                content_type="application/json",
                headers=headers,
            )
            self.assertEqual(rv.status_code, 404)

    def test_error_delete_token(self):
        matrix_registration.config.config = Config(data=BAD_CONFIG2)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(max_usage=True)

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            matrix_registration.config.config = Config(data=GOOD_CONFIG)
            rv = self.client.delete(
                "/api/token/" + test_token.name,
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 401)
            token_data = json.loads(rv.data.decode("utf8").replace("'", '"'))
            self.assertEqual(token_data["errcode"], "MR_BAD_SECRET")
            self.assertEqual(token_data["error"], "wrong shared secret")

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.delete(
                "/api/token/" + "nicememe",
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 404)
            token_data = json.loads(rv.data.decode("utf8"))
            self.assertEqual(token_data["errcode"], "MR_TOKEN_NOT_FOUND")
            self.assertEqual(token_data["error"], "token does not exist")

    @parameterized.expand(
        [
            [None, True, None],
            [datetime.fromisoformat("2020-12-24"), False, "2020-12-24 00:00:00"],
            [datetime.fromisoformat("2200-05-12"), True, "2200-05-12 00:00:00"],
        ]
    )
    def test_get_token(self, expiration_date, max_usage, parsed_date):
        matrix_registration.config.config = Config(data=BAD_CONFIG2)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(
                expiration_date=expiration_date, max_usage=max_usage
            )

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.get(
                "/api/token/" + test_token.name,
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 200)
            token_data = json.loads(rv.data.decode("utf8"))
            self.assertEqual(token_data["expiration_date"], parsed_date)
            self.assertEqual(token_data["max_usage"], max_usage)

    def test_error_get_token(self):
        matrix_registration.config.config = Config(data=BAD_CONFIG2)
        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            test_token = matrix_registration.tokens.tokens.new(max_usage=True)

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            rv = self.client.get(
                "/api/token/" + "nice_meme",
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 404)
            token_data = json.loads(rv.data.decode("utf8"))
            self.assertEqual(token_data["errcode"], "MR_TOKEN_NOT_FOUND")
            self.assertEqual(token_data["error"], "token does not exist")

            matrix_registration.config.config = Config(data=BAD_CONFIG2)

            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}
            matrix_registration.config.config = Config(data=GOOD_CONFIG)
            rv = self.client.patch(
                "/api/token/" + test_token.name,
                data=json.dumps(dict(disabled=True)),
                content_type="application/json",
                headers=headers,
            )

            self.assertEqual(rv.status_code, 401)
            token_data = json.loads(rv.data.decode("utf8").replace("'", '"'))
            self.assertEqual(token_data["errcode"], "MR_BAD_SECRET")
            self.assertEqual(token_data["error"], "wrong shared secret")

    def test_rate_limit_exempt(self):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)

        with self.app.app_context():
            matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens()
            secret = matrix_registration.config.config.admin_api_shared_secret
            headers = {"Authorization": "SharedSecret %s" % secret}

            for i in range(110):
                self.client.get("/api/token", headers=headers)

            rv = self.client.get("/api/token", headers=headers)
            self.assertEqual(rv.status_code, 429)

            for i in range(110):
                self.client.get("/health")

            rv = self.client.get("/health")
            self.assertEqual(rv.status_code, 200)


class ConfigTest(unittest.TestCase):
    def test_config_update(self):
        matrix_registration.config.config = Config(data=GOOD_CONFIG)
        self.assertEqual(matrix_registration.config.config.port, GOOD_CONFIG["port"])
        self.assertEqual(
            matrix_registration.config.config.server_location,
            GOOD_CONFIG["server_location"],
        )

        matrix_registration.config.config.update(BAD_CONFIG1)
        self.assertEqual(matrix_registration.config.config.port, BAD_CONFIG1["port"])
        self.assertEqual(
            matrix_registration.config.config.server_location,
            BAD_CONFIG1["server_location"],
        )

    def test_config_path(self):
        # BAD_CONFIG1_path = "x"
        good_config_path = "tests/test_config.yaml"

        with open(good_config_path, "w") as outfile:
            yaml.dump(GOOD_CONFIG, outfile, default_flow_style=False)

        matrix_registration.config.config = Config(path=good_config_path)
        self.assertIsNotNone(matrix_registration.config.config)
        os.remove(good_config_path)


class CliTest(unittest.TestCase):
    path = "tests/test_config.yaml"
    db = "tests/db.sqlite"

    def setUp(self):
        try:
            os.remove(self.db)
        except FileNotFoundError:
            pass
        with open(self.path, "w") as outfile:
            yaml.dump(GOOD_CONFIG, outfile, default_flow_style=False)

    def tearDown(self):
        os.remove(self.path)
        os.remove(self.db)

    def test_create_token(self):
        runner = create_app().test_cli_runner()
        generate = runner.invoke(cli, ["--config-path", self.path, "generate", "-m", 1])
        name1 = generate.output.strip()

        status = runner.invoke(cli, ["--config-path", self.path, "status", "-s", name1])
        valid, info_dict_string = status.output.strip().split("\n", 1)
        self.assertEqual(valid, "This token is valid")
        comparison_dict = {
            "name": name1,
            "used": 0,
            "expiration_date": None,
            "max_usage": 1,
            "disabled": False,
            "ips": [],
            "active": True,
        }
        self.assertEqual(json.loads(info_dict_string), comparison_dict)

        runner.invoke(cli, ["--config-path", self.path, "status", "-d", name1])
        status = runner.invoke(cli, ["--config-path", self.path, "status", "-s", name1])
        valid, info_dict_string = status.output.strip().split("\n", 1)
        self.assertEqual(valid, "This token is not valid")
        comparison_dict = {
            "name": name1,
            "used": 0,
            "expiration_date": None,
            "max_usage": 1,
            "disabled": True,
            "ips": [],
            "active": False,
        }
        self.assertEqual(json.loads(info_dict_string), comparison_dict)

        generate = runner.invoke(
            cli, ["--config-path", self.path, "generate", "-e", "2220-05-12"]
        )
        name2 = generate.output.strip()

        status = runner.invoke(cli, ["--config-path", self.path, "status", "-s", name2])
        valid, info_dict_string = status.output.strip().split("\n", 1)
        self.assertEqual(valid, "This token is valid")
        comparison_dict = {
            "name": name2,
            "used": 0,
            "expiration_date": "2220-05-12 00:00:00",
            "max_usage": 0,
            "disabled": False,
            "ips": [],
            "active": True,
        }
        self.assertEqual(json.loads(info_dict_string), comparison_dict)

        status = runner.invoke(cli, ["--config-path", self.path, "status", "-l"])
        list = status.output.strip()
        self.assertEqual(list, f"{name1}, {name2}")


if "logging" in sys.argv:
    logging.basicConfig(level=logging.DEBUG)

if __name__ == "__main__":
    unittest.main()


================================================
FILE: tox.ini
================================================
[tox]
envlist = py37,py38,p39
[testenv]
deps = coveralls
commands = coverage erase
       {envbindir}/python setup.py develop
       coverage run -p setup.py test
       coverage combine
       - coverage html
Download .txt
gitextract_w5avl08v/

├── .coveragerc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── codeql-analysis.yml
│       ├── docker.yml
│       ├── pypi.yml
│       └── tests.yml
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── alembic/
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
│       ├── 130b5c2275d8_update_ip_token_association.py
│       └── 140a25d5f185_create_tokens_table.py
├── alembic.ini
├── config.sample.yaml
├── default.nix
├── docker.nix
├── matrix_registration/
│   ├── __init__.py
│   ├── api.py
│   ├── app.py
│   ├── config.py
│   ├── config.schema.json
│   ├── constants.py
│   ├── limiter.py
│   ├── matrix_api.py
│   ├── static/
│   │   ├── css/
│   │   │   └── style.css
│   │   └── fonts/
│   │       └── NUNITO-LICENSE
│   ├── templates/
│   │   └── register.html
│   ├── tokens.py
│   ├── translation.py
│   ├── translations/
│   │   ├── messages.de.yaml
│   │   ├── messages.en.yaml
│   │   ├── messages.pt_BR.yaml
│   │   ├── messages.sv.yaml
│   │   └── messages.zh_Hans.yaml
│   └── wordlist.txt
├── resources/
│   ├── docker-run.sh
│   ├── docker-serve.sh
│   └── example.html
├── setup.py
├── shell.nix
├── tests/
│   ├── __init__.py
│   ├── context.py
│   ├── localhost.log.config
│   ├── localhost.signing.key
│   └── test_registration.py
└── tox.ini
Download .txt
SYMBOL INDEX (108 symbols across 12 files)

FILE: alembic/env.py
  function run_migrations_offline (line 42) | def run_migrations_offline():
  function run_migrations_online (line 66) | def run_migrations_online():

FILE: alembic/versions/130b5c2275d8_update_ip_token_association.py
  function upgrade (line 23) | def upgrade():
  function downgrade (line 48) | def downgrade():

FILE: alembic/versions/140a25d5f185_create_tokens_table.py
  function upgrade (line 24) | def upgrade():
  function downgrade (line 70) | def downgrade():

FILE: matrix_registration/api.py
  function validate_token (line 38) | def validate_token(form, token):
  function validate_username (line 59) | def validate_username(form, username):
  function validate_password (line 95) | def validate_password(form, password):
  class RegistrationForm (line 115) | class RegistrationForm(Form):
  function get_request_ips (line 145) | def get_request_ips(request):
  function verify_token (line 158) | def verify_token(token):
  function unauthorized (line 165) | def unauthorized():
  function element_logo (line 171) | def element_logo():
  function register (line 179) | def register():
  function create_account_from_form (line 222) | def create_account_from_form(form):
  function get_token (line 270) | def get_token(token):
  function get_tokens (line 277) | def get_tokens():
  function create_token (line 281) | def create_token(data):
  function update_token (line 307) | def update_token(token, data):
  function delete_token (line 321) | def delete_token(token):
  function health (line 334) | def health():
  function version (line 340) | def version():
  function token (line 352) | def token():
  function token_status (line 365) | def token_status(token):

FILE: matrix_registration/app.py
  function create_app (line 18) | def create_app(testing=False):
  function cli (line 40) | def cli(info, config_path):
  function run_server (line 57) | def run_server(info):
  function generate_token (line 78) | def generate_token(maximum, expires):
  function status_token (line 87) | def status_token(status, list, disable):

FILE: matrix_registration/config.py
  class Config (line 28) | class Config:
    method __init__ (line 35) | def __init__(self, path=None, data=None):
    method load (line 46) | def load(self):
    method load_from_file (line 59) | def load_from_file(self):
    method load_secrets (line 92) | def load_secrets(self):
    method check_config_locations (line 106) | def check_config_locations(self):
    method apply_options (line 138) | def apply_options(self):
    method ask_for_options (line 147) | def ask_for_options(self, sample_options):
    method update (line 174) | def update(self, data):

FILE: matrix_registration/limiter.py
  function get_real_user_ip (line 7) | def get_real_user_ip() -> str:
  function get_default_rate_limit (line 12) | def get_default_rate_limit() -> str:

FILE: matrix_registration/matrix_api.py
  function create_account (line 11) | def create_account(
  function _get_nonce (line 70) | def _get_nonce(server_location):

FILE: matrix_registration/tokens.py
  function random_readable_string (line 29) | def random_readable_string(length=3, wordlist=WORD_LIST_PATH):
  class IP (line 46) | class IP(db.Model):
    method __repr__ (line 51) | def __repr__(self):
  class Token (line 55) | class Token(db.Model):
    method __init__ (line 69) | def __init__(self, **kwargs):
    method __repr__ (line 78) | def __repr__(self):
    method toDict (line 81) | def toDict(self):
    method active (line 95) | def active(self):
    method use (line 103) | def use(self, ip_address=False):
    method disable (line 111) | def disable(self):
  class Tokens (line 118) | class Tokens:
    method __init__ (line 119) | def __init__(self):
    method __repr__ (line 124) | def __repr__(self):
    method toList (line 130) | def toList(self):
    method load (line 136) | def load(self):
    method get_token (line 145) | def get_token(self, token_name):
    method active (line 153) | def active(self, token_name):
    method use (line 160) | def use(self, token_name, ip_address=False):
    method update (line 169) | def update(self, token_name, data):
    method disable (line 185) | def disable(self, token_name):
    method delete (line 194) | def delete(self, token_name):
    method new (line 204) | def new(self, expiration_date=None, max_usage=False):

FILE: matrix_registration/translation.py
  function get_translations (line 12) | def get_translations(lang="en", replacements={}):
  function _get_translations (line 21) | def _get_translations(lang="en", replacements={}):

FILE: setup.py
  function read (line 11) | def read(*parts):
  function find_version (line 16) | def find_version(*file_paths):

FILE: tests/test_registration.py
  function mock_new_user (line 94) | def mock_new_user(username):
  function mocked__get_nonce (line 113) | def mocked__get_nonce(server_location):
  function mocked_requests_post (line 119) | def mocked_requests_post(*args, **kwargs):
  class TokensTest (line 182) | class TokensTest(unittest.TestCase):
    method setUp (line 183) | def setUp(self):
    method tearDown (line 196) | def tearDown(self):
    method test_random_readable_string (line 199) | def test_random_readable_string(self):
    method test_tokens_empty (line 205) | def test_tokens_empty(self):
    method test_tokens_disable (line 216) | def test_tokens_disable(self):
    method test_tokens_load (line 239) | def test_tokens_load(self):
    method test_tokens_new (line 299) | def test_tokens_new(self, expiration_date, max_usage):
    method test_tokens_active_form (line 328) | def test_tokens_active_form(self, expiration_date, max_usage, times_us...
    method test_tokens_active (line 353) | def test_tokens_active(self, expiration_date, active):
    method test_tokens_repr (line 371) | def test_tokens_repr(self, name):
    method test_token_repr (line 377) | def test_token_repr(self):
  class ApiTest (line 397) | class ApiTest(unittest.TestCase):
    method setUp (line 398) | def setUp(self):
    method tearDown (line 411) | def tearDown(self):
    method test_register (line 444) | def test_register(
    method test_register_wrong_hs (line 480) | def test_register_wrong_hs(self, mock_get, mock_nonce):
    method test_register_wrong_secret (line 503) | def test_register_wrong_secret(self, mock_get, mock_nonce):
    method test_get_tokens (line 522) | def test_get_tokens(self):
    method test_error_get_tokens (line 541) | def test_error_get_tokens(self):
    method test_post_token (line 568) | def test_post_token(self, expiration_date, max_usage, parsed_date):
    method test_error_post_token (line 594) | def test_error_post_token(self):
    method test_patch_token (line 633) | def test_patch_token(self):
    method test_error_patch_token (line 655) | def test_error_patch_token(self):
    method test_delete_token (line 705) | def test_delete_token(self):
    method test_error_delete_token (line 735) | def test_error_delete_token(self):
    method test_get_token (line 776) | def test_get_token(self, expiration_date, max_usage, parsed_date):
    method test_error_get_token (line 798) | def test_error_get_token(self):
    method test_rate_limit_exempt (line 834) | def test_rate_limit_exempt(self):
  class ConfigTest (line 855) | class ConfigTest(unittest.TestCase):
    method test_config_update (line 856) | def test_config_update(self):
    method test_config_path (line 871) | def test_config_path(self):
  class CliTest (line 883) | class CliTest(unittest.TestCase):
    method setUp (line 887) | def setUp(self):
    method tearDown (line 895) | def tearDown(self):
    method test_create_token (line 899) | def test_create_token(self):
Condensed preview — 54 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (167K chars).
[
  {
    "path": ".coveragerc",
    "chars": 35,
    "preview": "[run]\nsource = matrix_registration\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 2311,
    "preview": "name: \"🐛 Bug report\"\ndescription: Report errors or unexpected behavior\nlabels: \n- bug\nbody:\n- type: markdown\n  attribute"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 508,
    "preview": "blank_issues_enabled: true\ncontact_links:\n  - name: \"\\U0001F4DA Explanation to all config entries\"\n    url: https://gith"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 774,
    "preview": "name: \"⭐ Feature / enhancement request\"\ndescription: Suggest an idea for this project\nlabels: \n- enhancement\nbody:\n- typ"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 2436,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 6876,
    "preview": "name: Publish Docker Image\non: [ push, pull_request ]\n\njobs:\n  build_image:\n    name: Build Docker Image\n    runs-on: ub"
  },
  {
    "path": ".github/workflows/pypi.yml",
    "chars": 781,
    "preview": "name: Publish Python Package\n\non:\n  push:\n    # Sequence of patterns matched against refs/tags\n    tags:\n      - 'v*' # "
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 1095,
    "preview": "name: Tests\n\non:\n  push:\n    branches:\n    - master\n  pull_request:\n    branches:\n    - master\n\njobs:\n  build:\n\n    runs"
  },
  {
    "path": ".gitignore",
    "chars": 1539,
    "preview": "# Project specific\n*.yaml\n*.conf\n!config.sample.yaml\n!tests/test_config.yaml\nresult\ndata/\n\n!matrix_registration/translat"
  },
  {
    "path": ".travis.yml",
    "chars": 300,
    "preview": "language: python\nmatrix:\n  include:\n    - python: 3.7\n      dist: focal\n      sudo: true\n    - python: 3.8\n      dist: f"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3345,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1836,
    "preview": "# CONTRIBUTING\n## Code of Conduct\nSee [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)\n## How Can I Contribute?\n### Issues\nFilli"
  },
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "MIT License\n\nCopyright (c) 2018 Jona Abdinghoff\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "README.md",
    "chars": 6155,
    "preview": "<img src=\"resources/logo.png\" width=\"300\">\n\n[![Build Status](https://travis-ci.org/ZerataX/matrix-registration.svg?branc"
  },
  {
    "path": "SECURITY.md",
    "chars": 839,
    "preview": "# Security Policy\n\n## Supported Versions\nThis project is still in active develoment so be aware that only the latest ver"
  },
  {
    "path": "alembic/README",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "alembic/env.py",
    "chars": 2642,
    "preview": "from logging.config import fileConfig\n\nfrom sqlalchemy import engine_from_config\nfrom sqlalchemy import pool\n\nfrom alemb"
  },
  {
    "path": "alembic/script.py.mako",
    "chars": 494,
    "preview": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom ale"
  },
  {
    "path": "alembic/versions/130b5c2275d8_update_ip_token_association.py",
    "chars": 2206,
    "preview": "'''update ip token association\n\nRevision ID: 130b5c2275d8\nRevises: 140a25d5f185\nCreate Date: 2021-07-10 20:40:46.937634\n"
  },
  {
    "path": "alembic/versions/140a25d5f185_create_tokens_table.py",
    "chars": 2223,
    "preview": "\"\"\"create tokens table\n\nRevision ID: 140a25d5f185\nRevises: \nCreate Date: 2020-12-12 01:44:28.195736\n\n\"\"\"\nfrom alembic im"
  },
  {
    "path": "alembic.ini",
    "chars": 1592,
    "preview": "# A generic, single database configuration.\n\n[alembic]\n# path to migration scripts\nscript_location = alembic\n\n# template"
  },
  {
    "path": "config.sample.yaml",
    "chars": 1516,
    "preview": "server_location: 'http://localhost:8008'\nserver_name: 'matrix.org'\nregistration_shared_secret: 'RegistrationSharedSecret"
  },
  {
    "path": "default.nix",
    "chars": 503,
    "preview": "{ pkgs ? import <nixpkgs> { } }:\nwith pkgs.python3.pkgs;\n\nbuildPythonPackage rec {\n  name = \"matrix-registration\";\n  src"
  },
  {
    "path": "docker.nix",
    "chars": 1367,
    "preview": "{ pkgs ? import <nixpkgs> { }, tag ? \"latest\" }:\n\nlet\n  matrix-registration-config = \"/data/config.yaml\";\n\n  python3 = l"
  },
  {
    "path": "matrix_registration/__init__.py",
    "chars": 117,
    "preview": "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",
    "chars": 11470,
    "preview": "# Standard library imports...\nimport logging\nimport os\nimport re\nfrom datetime import datetime\n\n# Third-party imports..."
  },
  {
    "path": "matrix_registration/app.py",
    "chars": 2839,
    "preview": "import json\nimport logging\nimport logging.config\nimport os\n\nimport click\nfrom flask import Flask\nfrom flask.cli import F"
  },
  {
    "path": "matrix_registration/config.py",
    "chars": 5723,
    "preview": "# Standard library imports...\n# from collections import namedtuple\nimport logging\nimport os\nimport sys\n\n# Third-party im"
  },
  {
    "path": "matrix_registration/config.schema.json",
    "chars": 1882,
    "preview": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"server_location\": {\n      \"type\": \"string\",\n      \"format\": \"uri\",\n      \"p"
  },
  {
    "path": "matrix_registration/constants.py",
    "chars": 711,
    "preview": "# Standard library imports...\nimport os\nimport site\nimport sys\n\n# Third-party imports...\nfrom appdirs import user_config"
  },
  {
    "path": "matrix_registration/limiter.py",
    "chars": 443,
    "preview": "from flask import request\nfrom flask_limiter import Limiter\n\nfrom . import config\n\n\ndef get_real_user_ip() -> str:\n    \""
  },
  {
    "path": "matrix_registration/matrix_api.py",
    "chars": 1852,
    "preview": "# Standard library imports...\nimport hashlib\nimport hmac\nimport requests\n\nimport logging\n\nlogger = logging.getLogger(__n"
  },
  {
    "path": "matrix_registration/static/css/style.css",
    "chars": 4168,
    "preview": "html,\nbody {\n  height: 100%;\n  margin: 0;\n  font-family: 'Nunito', sans-serif;\n}\n\nbody {\n  background-size: cover;\n  bac"
  },
  {
    "path": "matrix_registration/static/fonts/NUNITO-LICENSE",
    "chars": 4385,
    "preview": "Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)\n\nThis Font Software is licensed under "
  },
  {
    "path": "matrix_registration/templates/register.html",
    "chars": 11028,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width initial-scal"
  },
  {
    "path": "matrix_registration/tokens.py",
    "chars": 5913,
    "preview": "# Standard library imports...\nfrom datetime import datetime\nimport logging\nimport random\n\nfrom flask_sqlalchemy import S"
  },
  {
    "path": "matrix_registration/translation.py",
    "chars": 1069,
    "preview": "import os\nimport re\n\nimport yaml\n\nfrom .constants import __location__\n\n\nreplace_pattern = re.compile(r\"{{\\s*(?P<name>.[a"
  },
  {
    "path": "matrix_registration/translations/messages.de.yaml",
    "chars": 1250,
    "preview": "weblate:\n  server_registration: \"{{ server_name }} Registrierung\"\n  register_account: \"Registriere einen Account auf {{ "
  },
  {
    "path": "matrix_registration/translations/messages.en.yaml",
    "chars": 1137,
    "preview": "weblate:\n  server_registration: \"{{ server_name }} registration\"\n  register_account: \"register an account on {{ server_n"
  },
  {
    "path": "matrix_registration/translations/messages.pt_BR.yaml",
    "chars": 1201,
    "preview": "weblate:\n  error_long: Ocorreu um erro enquanto tentávamos fazer seu registro.\n  register: registrar\n  confirm: Confirma"
  },
  {
    "path": "matrix_registration/translations/messages.sv.yaml",
    "chars": 1140,
    "preview": "weblate:\n  welcome: Välkommen\n  homeserver_error: Hemserverfel\n  username_error: Användarnamnsfel\n  password_error: Löse"
  },
  {
    "path": "matrix_registration/translations/messages.zh_Hans.yaml",
    "chars": 704,
    "preview": "weblate:\n  requires_token: 注册需要一个秘密令牌\n  welcome: 欢迎\n  homeserver_error: 主服务器错误\n  username_error: 用户名错误\n  password_error:"
  },
  {
    "path": "matrix_registration/wordlist.txt",
    "chars": 11027,
    "preview": "acrobat\nafrica\nalaska\nalbert\nalbino\nalbum\nalcohol\nalex\nalpha\namadeus\namanda\namazon\namerica\nanalog\nanimal\nantenna\nantonio"
  },
  {
    "path": "resources/docker-run.sh",
    "chars": 139,
    "preview": "#!/bin/sh\n\ndocker run \\\n  -it --rm \\\n  --user \"$(id -u):$(id -g)\" \\\n  --volume $(pwd)/data:/data  \\\n  matrix-registratio"
  },
  {
    "path": "resources/docker-serve.sh",
    "chars": 171,
    "preview": "#!/bin/sh\n\ndocker run \\\n  -d \\\n  --user \"$(id -u):$(id -g)\" \\\n  --network matrix \\\n  --publish 5000:5000/tcp \\\n  --volum"
  },
  {
    "path": "resources/example.html",
    "chars": 3315,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Matrix Registration</title>\n  <style>\n    input:invalid "
  },
  {
    "path": "setup.py",
    "chars": 2760,
    "preview": "#!/usr/bin/env python\nimport codecs\nimport os\nimport re\nimport setuptools\nimport glob\n\nhere = os.path.abspath(os.path.di"
  },
  {
    "path": "shell.nix",
    "chars": 224,
    "preview": "{ pkgs ? import <nixpkgs> { } }:\n\n(let matrix-registration = pkgs.callPackage ./default.nix { inherit pkgs; };\nin pkgs.p"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/context.py",
    "chars": 133,
    "preview": "import os\nimport sys\n\nsys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\")))\n\nimport matrix_"
  },
  {
    "path": "tests/localhost.log.config",
    "chars": 337,
    "preview": "version: 1\nformatters:\n  precise:\n    format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(mes"
  },
  {
    "path": "tests/localhost.signing.key",
    "chars": 59,
    "preview": "ed25519 a_snwG JXtOu5WvTJOETWmItITGUlnqWy6WO4Ovew2flTRYD90\n"
  },
  {
    "path": "tests/test_registration.py",
    "chars": 36801,
    "preview": "# -*- coding: utf-8 -*-\n# Standard library imports...\nimport hashlib\nimport hmac\nimport json\nimport logging.config\nimpor"
  },
  {
    "path": "tox.ini",
    "chars": 210,
    "preview": "[tox]\nenvlist = py37,py38,p39\n[testenv]\ndeps = coveralls\ncommands = coverage erase\n       {envbindir}/python setup.py de"
  }
]

About this extraction

This page contains the full source code of the ZerataX/matrix-registration GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 54 files (151.0 KB), approximately 38.6k tokens, and a symbol index with 108 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!