From 5e50fd1640c98dcc59853a509b31bdd5559baf7d Mon Sep 17 00:00:00 2001
From: lza_menace
Date: Sun, 27 Nov 2022 11:59:18 -0800
Subject: [PATCH] init
---
.gitignore | 4 +
Dockerfile-monero | 38 ++++
LICENSE | 21 +++
Makefile | 33 ++++
README.md | 7 +
bin/run_wallet.sh | 38 ++++
docker-compose.yaml | 16 ++
env-example | 12 ++
manage.sh | 31 ++++
nerochan/__init__.py | 1 +
nerochan/app.py | 10 ++
nerochan/cli.py | 12 ++
nerochan/config.py | 35 ++++
nerochan/decorators.py | 25 +++
nerochan/factory.py | 38 ++++
nerochan/filters.py | 25 +++
nerochan/forms.py | 47 +++++
nerochan/helpers.py | 19 ++
nerochan/library/__init__.py | 0
nerochan/library/cache.py | 35 ++++
nerochan/library/market.py | 16 ++
nerochan/models.py | 103 +++++++++++
nerochan/routes/__init__.py | 0
nerochan/routes/api.py | 11 ++
nerochan/routes/auth.py | 102 +++++++++++
nerochan/routes/main.py | 30 ++++
nerochan/routes/post.py | 27 +++
nerochan/static/css/main.css | 0
nerochan/static/css/noty-relax.css | 46 +++++
nerochan/static/css/noty.css | 222 ++++++++++++++++++++++++
nerochan/static/css/noty.css.map | 1 +
nerochan/static/images/monero-logo.png | Bin 0 -> 11912 bytes
nerochan/templates/auth/challenge.html | 11 ++
nerochan/templates/auth/login.html | 8 +
nerochan/templates/auth/register.html | 8 +
nerochan/templates/includes/base.html | 18 ++
nerochan/templates/includes/debug.html | 9 +
nerochan/templates/includes/footer.html | 18 ++
nerochan/templates/includes/form.html | 18 ++
nerochan/templates/includes/head.html | 22 +++
nerochan/templates/includes/header.html | 20 +++
nerochan/templates/index.html | 9 +
nerochan/templates/post/show.html | 25 +++
requirements.txt | 17 ++
44 files changed, 1188 insertions(+)
create mode 100644 .gitignore
create mode 100644 Dockerfile-monero
create mode 100644 LICENSE
create mode 100644 Makefile
create mode 100644 README.md
create mode 100644 bin/run_wallet.sh
create mode 100644 docker-compose.yaml
create mode 100644 env-example
create mode 100755 manage.sh
create mode 100644 nerochan/__init__.py
create mode 100755 nerochan/app.py
create mode 100644 nerochan/cli.py
create mode 100644 nerochan/config.py
create mode 100644 nerochan/decorators.py
create mode 100644 nerochan/factory.py
create mode 100644 nerochan/filters.py
create mode 100644 nerochan/forms.py
create mode 100644 nerochan/helpers.py
create mode 100644 nerochan/library/__init__.py
create mode 100644 nerochan/library/cache.py
create mode 100644 nerochan/library/market.py
create mode 100644 nerochan/models.py
create mode 100644 nerochan/routes/__init__.py
create mode 100644 nerochan/routes/api.py
create mode 100644 nerochan/routes/auth.py
create mode 100644 nerochan/routes/main.py
create mode 100644 nerochan/routes/post.py
create mode 100644 nerochan/static/css/main.css
create mode 100644 nerochan/static/css/noty-relax.css
create mode 100644 nerochan/static/css/noty.css
create mode 100644 nerochan/static/css/noty.css.map
create mode 100644 nerochan/static/images/monero-logo.png
create mode 100644 nerochan/templates/auth/challenge.html
create mode 100644 nerochan/templates/auth/login.html
create mode 100644 nerochan/templates/auth/register.html
create mode 100644 nerochan/templates/includes/base.html
create mode 100644 nerochan/templates/includes/debug.html
create mode 100644 nerochan/templates/includes/footer.html
create mode 100644 nerochan/templates/includes/form.html
create mode 100644 nerochan/templates/includes/head.html
create mode 100644 nerochan/templates/includes/header.html
create mode 100644 nerochan/templates/index.html
create mode 100644 nerochan/templates/post/show.html
create mode 100644 requirements.txt
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4a607f6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.venv
+.env
+data
+__pycache__
diff --git a/Dockerfile-monero b/Dockerfile-monero
new file mode 100644
index 0000000..d61ada7
--- /dev/null
+++ b/Dockerfile-monero
@@ -0,0 +1,38 @@
+FROM ubuntu:22.04
+
+ENV MONERO_HASH 937dfcc48d91748dd2e8f58714dfc45d17a0959dff33fc7385bbe06344ff2c16
+ENV MONERO_DL_URL https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.1.1.tar.bz2
+ENV MONERO_DL_FILE monero.tar.bz2
+ENV MONERO_SUMS_FILE sha256sums
+
+WORKDIR /opt/monero
+
+# Update system and install dependencies
+# Download Monero binaries from Github
+# Confirm hashes match
+# Install binaries to system path
+# Clean up
+
+RUN apt-get update
+RUN apt-get upgrade -y
+RUN apt-get install -y tar wget bzip2
+
+RUN wget -qO ${MONERO_DL_FILE} ${MONERO_DL_URL}
+RUN echo "${MONERO_HASH} ${MONERO_DL_FILE}" > ${MONERO_SUMS_FILE} \
+ && sha256sum -c ${MONERO_SUMS_FILE}; \
+ if [ "$?" -eq 0 ]; \
+ then \
+ echo -e "[+] Hashes match - proceeding with container build"; \
+ else \
+ echo -e "[!] Hashes do not match - exiting"; \
+ exit 5; \
+ fi \
+ && mkdir ./tmp \
+ && tar xvf ${MONERO_DL_FILE} -C ./tmp --strip 1 \
+ && mv ./tmp/* /usr/local/bin/ \
+ && rm -rf ./tmp ${MONERO_SUMS_FILE} ${MONERO_DL_FILE}
+
+WORKDIR /tmp
+COPY bin/run_wallet.sh /run_wallet.sh
+
+WORKDIR /data
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..29da2e3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 @lza_menace (https://lzahq.tech)
+
+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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e8ab3aa
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,33 @@
+.PHONY: format help
+
+# Help system from https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
+.DEFAULT_GOAL := help
+
+help:
+ @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
+
+setup: ## Establish local environment with dependencies installed
+ python3 -m venv .venv
+ .venv/bin/pip install -r requirements.txt
+ mkdir -p data/uploads
+
+build: ## Build containers
+ docker-compose build
+
+up: ## Start containers
+ docker-compose up -d
+
+down: ## Stop containers
+ docker-compose down
+
+dbshell:
+ docker-compose exec db psql -U nerochan
+
+shell: ## Start Flask CLI shell
+ ./manage.sh shell
+
+init: ## Initialize SQLite DB
+ ./manage.sh init
+
+dev: ## Start Flask development web server
+ ./manage.sh run
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4a2fa0a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# nerochan
+
+Post your best neroChans. Get tipped.
+
+No middle-men. p2p tips.
+
+No passwords. Wallet signing authentication.
diff --git a/bin/run_wallet.sh b/bin/run_wallet.sh
new file mode 100644
index 0000000..8fbb371
--- /dev/null
+++ b/bin/run_wallet.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+export RPC_CREDS="${2}"
+export DAEMON_ADDRESS="${3}"
+
+# Define the network we plan to operate our wallet in
+if [[ "${1}" == "stagenet" ]]; then
+ export NETWORK=--stagenet
+ export PORT=38081
+elif [[ "${1}" == "testnet" ]]; then
+ export NETWORK=--testnet
+ export PORT=28081
+else
+ export NETWORK=
+ export PORT=18089
+fi
+
+# Create new wallet if it doesn't exist
+if [[ ! -d /data/wallet ]]; then
+ monero-wallet-cli ${NETWORK} \
+ --generate-new-wallet /data/wallet \
+ --daemon-address ${DAEMON_ADDRESS} \
+ --trusted-daemon \
+ --use-english-language-names \
+ --mnemonic-language English
+fi
+
+# Run RPC wallet
+monero-wallet-rpc ${NETWORK} \
+ --daemon-address ${DAEMON_ADDRESS} \
+ --wallet-file /data/wallet \
+ --password "" \
+ --rpc-login ${RPC_CREDS} \
+ --rpc-bind-port 8000 \
+ --rpc-bind-ip 0.0.0.0 \
+ --confirm-external-bind \
+ --log-file /data/wallet-rpc.log \
+ --trusted-daemon
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..8e4693e
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,16 @@
+version: '3'
+services:
+ cache:
+ image: redis:latest
+ ports:
+ - 127.0.0.1:6379:6379
+ wallet:
+ build:
+ context: .
+ dockerfile: Dockerfile-monero
+ ports:
+ - 127.0.0.1:${XMR_WALLET_RPC_PORT:-8000}:${XMR_WALLET_RPC_PORT:-8000}
+ volumes:
+ - ${DATA_DIR:-./data/wallet}:/data
+ command:
+ bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}" "${XMR_DAEMON_URI}"
diff --git a/env-example b/env-example
new file mode 100644
index 0000000..e00c872
--- /dev/null
+++ b/env-example
@@ -0,0 +1,12 @@
+DB_PATH=./data/sqlite.db
+
+XMR_WALLET_RPC_USER=xxxxxxxxxx
+XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx
+XMR_WALLET_RPC_PORT=8000
+XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089
+XMR_WALLET_NETWORK=stagenet
+
+SITE_NAME=nerochan
+SECRET_KEY=xxxxxxxxxxxxxxxxxxx
+STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx
+SERVER_NAME=127.0.0.1:5000
diff --git a/manage.sh b/manage.sh
new file mode 100755
index 0000000..6d351fa
--- /dev/null
+++ b/manage.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+source .venv/bin/activate
+export FLASK_APP=nerochan/app.py
+export FLASK_SECRETS=config.py
+export FLASK_DEBUG=1
+export FLASK_ENV=development
+
+# override
+source .env
+
+if [[ ${1} == "prod" ]];
+then
+ export FLASK_DEBUG=0
+ export FLASK_ENV=production
+ export BASE=./data/gunicorn
+ mkdir -p $BASE
+ pgrep -F $BASE/gunicorn.pid
+ if [[ $? != 0 ]]; then
+ gunicorn \
+ --bind 127.0.0.1:4000 "nerochan.app:app" \
+ --daemon \
+ --log-file $BASE/gunicorn.log \
+ --pid $BASE/gunicorn.pid \
+ --reload
+ sleep 2
+ echo "Started gunicorn on 127.0.0.1:4000 with pid $(cat $BASE/gunicorn.pid)"
+ fi
+else
+ flask $@
+fi
diff --git a/nerochan/__init__.py b/nerochan/__init__.py
new file mode 100644
index 0000000..be1e109
--- /dev/null
+++ b/nerochan/__init__.py
@@ -0,0 +1 @@
+from nerochan.factory import create_app
\ No newline at end of file
diff --git a/nerochan/app.py b/nerochan/app.py
new file mode 100755
index 0000000..7444507
--- /dev/null
+++ b/nerochan/app.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python3
+
+from nerochan import create_app
+
+
+app = create_app()
+
+
+if __name__ == '__main__':
+ app.run(debug=True, use_reloader=True)
diff --git a/nerochan/cli.py b/nerochan/cli.py
new file mode 100644
index 0000000..f2b28ea
--- /dev/null
+++ b/nerochan/cli.py
@@ -0,0 +1,12 @@
+import click
+
+
+def cli(app):
+ @app.cli.command('init')
+ def init():
+ import peewee
+ from nerochan.models import db
+ model = peewee.Model.__subclasses__()
+ db.create_tables(model)
+
+ return app
diff --git a/nerochan/config.py b/nerochan/config.py
new file mode 100644
index 0000000..ffc5cd4
--- /dev/null
+++ b/nerochan/config.py
@@ -0,0 +1,35 @@
+from secrets import token_urlsafe
+from datetime import timedelta
+from os import getenv
+
+from dotenv import load_dotenv
+
+
+load_dotenv()
+
+# Site meta
+SITE_NAME = getenv('SITE_NAME', 'nerochan')
+SECRET_KEY = getenv('SECRET_KEY', token_urlsafe(12))
+SERVER_NAME = getenv('SERVER_NAME', '127.0.0.1:5000')
+
+# Crypto RPC
+XMR_WALLET_PASS = getenv('XMR_WALLET_PASS')
+XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER')
+XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS')
+XMR_WALLET_RPC_PORT = getenv('XMR_WALLET_RPC_PORT', 8000)
+XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK')
+
+# Database
+DB_PATH = getenv('DB_PATH', './data/sqlite.db')
+
+# Redis
+REDIS_HOST = getenv('REDIS_HOST', 'localhost')
+REDIS_PORT = getenv('REDIS_PORT', 6379)
+
+# Sessions
+SESSION_LENGTH = int(getenv('SESSION_LENGTH', 300))
+PERMANENT_SESSION_LIFETIME = timedelta(minutes=SESSION_LENGTH)
+MAX_CONTENT_LENGTH = 50 * 1024 * 1024
+
+# Development
+TEMPLATES_AUTO_RELOAD = True
diff --git a/nerochan/decorators.py b/nerochan/decorators.py
new file mode 100644
index 0000000..409128e
--- /dev/null
+++ b/nerochan/decorators.py
@@ -0,0 +1,25 @@
+from flask import session, redirect, url_for, flash
+from flask_login import current_user
+from functools import wraps
+from nerochan.models import User, CreatorProfile, BackerProfile, Subscription
+
+
+# def login_required(f):
+# @wraps(f)
+# def decorated_function(*args, **kwargs):
+# if "auth" not in session or not session["auth"]:
+# return redirect(url_for("auth.login"))
+# return f(*args, **kwargs)
+# return decorated_function
+
+def subscription_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ print(current_user)
+ # m = Moderator.filter(username=session["auth"]["preferred_username"])
+ # if m:
+ # return f(*args, **kwargs)
+ # else:
+ # flash("You are not a moderator")
+ # return redirect(url_for("index"))
+ return decorated_function
diff --git a/nerochan/factory.py b/nerochan/factory.py
new file mode 100644
index 0000000..dba09be
--- /dev/null
+++ b/nerochan/factory.py
@@ -0,0 +1,38 @@
+from flask import Flask
+from flask_login import LoginManager
+
+from nerochan.cli import cli
+
+
+def setup_db(app: Flask):
+ import peewee
+ models = peewee.Model.__subclasses__()
+ for m in models:
+ m.create_table()
+
+
+def create_app():
+ app = Flask(__name__)
+ app = cli(app)
+ app.config.from_envvar('FLASK_SECRETS')
+
+ # Login manager
+ login_manager = LoginManager(app)
+ login_manager.login_view = 'auth.login'
+ login_manager.logout_view = 'auth.logout'
+
+ @login_manager.user_loader
+ def load_user(user_id):
+ from nerochan.models import User
+ return User.get_or_none(user_id)
+
+ with app.app_context():
+ from nerochan.routes import api, auth, post, main
+ from nerochan import filters
+ app.register_blueprint(main.bp)
+ app.register_blueprint(api.bp)
+ app.register_blueprint(auth.bp)
+ app.register_blueprint(post.bp)
+ app.register_blueprint(filters.bp)
+
+ return app
diff --git a/nerochan/filters.py b/nerochan/filters.py
new file mode 100644
index 0000000..ec03734
--- /dev/null
+++ b/nerochan/filters.py
@@ -0,0 +1,25 @@
+from datetime import datetime
+
+import arrow
+from monero import numbers
+from flask import Blueprint, current_app
+
+
+bp = Blueprint('filters', 'filters')
+
+
+@bp.app_template_filter('humanize')
+def humanize(d):
+ return arrow.get(d).humanize()
+
+@bp.app_template_filter('ts')
+def from_ts(v):
+ return datetime.fromtimestamp(v)
+
+@bp.app_template_filter('xmr_block_explorer')
+def xmr_block_explorer(v):
+ return f'https://www.exploremonero.com/transaction/{v}'
+
+@bp.app_template_filter('from_atomic')
+def from_atomic(amt):
+ return numbers.from_atomic(amt)
diff --git a/nerochan/forms.py b/nerochan/forms.py
new file mode 100644
index 0000000..e8f3984
--- /dev/null
+++ b/nerochan/forms.py
@@ -0,0 +1,47 @@
+from flask_wtf import FlaskForm
+from wtforms import StringField, FloatField
+from wtforms.validators import DataRequired, ValidationError
+from monero.address import address
+
+from nerochan import config
+
+
+def is_valid_xmr_address(form, field):
+ try:
+ # Ensure the provided address is valid address/subaddress/integrated address
+ a = address(field.data)
+ print(config.XMR_WALLET_NETWORK)
+ # Ensure the provided address matches the network that the application's wallet is using
+ if not config.XMR_WALLET_NETWORK.startswith(a.net):
+ raise ValidationError('Provided Monero address does not match the configured network. Application: {}. Provided: {}'.format(
+ config.XMR_WALLET_NETWORK.replace('net', ''), a.net
+ ))
+ except ValueError:
+ raise ValidationError('Invalid Monero address provided')
+
+
+class UserRegistration(FlaskForm):
+ handle = StringField('Handle:', validators=[DataRequired()], render_kw={'placeholder': 'online handle', 'class': 'form-control', 'type': 'text'})
+ wallet_address = StringField('Wallet Address:', validators=[DataRequired(), is_valid_xmr_address], render_kw={'placeholder': 'monero wallet address', 'class': 'form-control', 'type': 'text'})
+
+
+class UserLogin(FlaskForm):
+ handle = StringField('Handle:', validators=[DataRequired()], render_kw={'placeholder': 'online handle', 'class': 'form-control', 'type': 'text'})
+
+
+class UserChallenge(FlaskForm):
+ signature = StringField('Signature:', validators=[DataRequired()], render_kw={'placeholder': 'signed data', 'class': 'form-control', 'type': 'text'})
+
+
+class ConfirmPlatformSubscription(FlaskForm):
+ tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={'placeholder': 'TX ID', 'class': 'form-control', 'type': 'text'})
+ tx_key = StringField('TX Key:', validators=[DataRequired()], render_kw={'placeholder': 'TX Key', 'class': 'form-control', 'type': 'text'})
+
+
+class ConfirmCreatorSubscription(ConfirmPlatformSubscription):
+ wallet_address = StringField('Wallet Address:', validators=[DataRequired(), is_valid_xmr_address], render_kw={'placeholder': 'monero wallet address', 'class': 'form-control', 'type': 'text'})
+
+
+class CreateSubscription(FlaskForm):
+ price_xmr = FloatField('Price (XMR):', validators=[DataRequired()], render_kw={'placeholder': '.5', 'class': 'form-control', 'type': 'text'})
+ number_days = FloatField('Length (Days)', validators=[DataRequired()], render_kw={'placeholder': '30', 'class': 'form-control', 'type': 'text'})
diff --git a/nerochan/helpers.py b/nerochan/helpers.py
new file mode 100644
index 0000000..4f4ad13
--- /dev/null
+++ b/nerochan/helpers.py
@@ -0,0 +1,19 @@
+import peewee as pw
+import playhouse.postgres_ext as pwpg
+from monero.wallet import Wallet
+
+from nerochan import config
+
+
+def make_wallet_rpc(method, data={}):
+ try:
+ w = Wallet(
+ port=config.XMR_WALLET_RPC_PORT,
+ user=config.XMR_WALLET_RPC_USER,
+ password=config.XMR_WALLET_RPC_PASS,
+ timeout=3
+ )
+ res = w._backend.raw_request(method, data)
+ return res
+ except Exception as e:
+ raise e
diff --git a/nerochan/library/__init__.py b/nerochan/library/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nerochan/library/cache.py b/nerochan/library/cache.py
new file mode 100644
index 0000000..6dc9d37
--- /dev/null
+++ b/nerochan/library/cache.py
@@ -0,0 +1,35 @@
+from json import loads as json_loads
+from json import dumps as json_dumps
+from datetime import timedelta
+
+from redis import Redis
+
+from nerochan.library.market import get_market_data
+from nerochan import config
+
+
+class Cache(object):
+ def __init__(self):
+ self.redis = Redis(host=config.REDIS_HOST, port=config.REDIS_PORT)
+
+ def store_data(self, item_name, expiration_minutes, data):
+ self.redis.setex(
+ item_name,
+ timedelta(minutes=expiration_minutes),
+ value=data
+ )
+
+ def get_coin_price(self, coin_name):
+ key_name = f'{coin_name}_price'
+ data = self.redis.get(key_name)
+ if data:
+ return json_loads(data)
+ else:
+ d = get_market_data(coin_name)
+ data = {
+ key_name: d['market_data']['current_price'],
+ }
+ self.store_data(key_name, 4, json_dumps(data))
+ return data
+
+cache = Cache()
diff --git a/nerochan/library/market.py b/nerochan/library/market.py
new file mode 100644
index 0000000..1763905
--- /dev/null
+++ b/nerochan/library/market.py
@@ -0,0 +1,16 @@
+from requests import get as r_get
+
+
+def get_market_data(coin_name="monero"):
+ data = {
+ 'localization': False,
+ 'tickers': False,
+ 'market_data': True,
+ 'community_data': False,
+ 'developer_data': False,
+ 'sparkline': False
+ }
+ headers = {'accept': 'application/json'}
+ url = f'https://api.coingecko.com/api/v3/coins/{coin_name}'
+ r = r_get(url, headers=headers, data=data)
+ return r.json()
diff --git a/nerochan/models.py b/nerochan/models.py
new file mode 100644
index 0000000..f753a55
--- /dev/null
+++ b/nerochan/models.py
@@ -0,0 +1,103 @@
+from datetime import datetime
+from secrets import token_urlsafe
+
+import peewee as pw
+
+from nerochan import config
+
+
+db = pw.SqliteDatabase(
+ config.DB_PATH
+)
+
+def gen_challenge():
+ return token_urlsafe().replace('-', '').replace('_', '')
+
+
+class User(pw.Model):
+ """
+ User model is for base user management and reporting.
+ """
+ id = pw.AutoField()
+ register_date = pw.DateTimeField(default=datetime.utcnow)
+ last_login_date = pw.DateTimeField(default=datetime.utcnow)
+ handle = pw.CharField(unique=True)
+ wallet_address = pw.CharField(unique=True)
+ challenge = pw.CharField(default=gen_challenge)
+ is_admin = pw.BooleanField(default=False)
+ is_mod = pw.BooleanField(default=False)
+
+ @property
+ def is_authenticated(self):
+ return True
+
+ @property
+ def is_active(self):
+ return True
+
+ @property
+ def is_anonymous(self):
+ return False
+
+ def get_id(self):
+ return self.id
+
+ def regenerate_challenge(self):
+ self.challenge = gen_challenge()
+ self.save()
+
+ class Meta:
+ database = db
+
+
+class Profile(pw.Model):
+ """
+ Profile model is for users to provide metadata about
+ themselves; Creators for their fans or even just the general public.
+ Links to social media, contact info, portfolio sites, etc
+ should go in here.
+ """
+ id = pw.AutoField()
+ user = pw.ForeignKeyField(User)
+ create_date = pw.DateTimeField(default=datetime.utcnow)
+ website = pw.CharField(unique=True, null=True)
+ twitter_handle = pw.CharField(unique=True, null=True)
+ bio = pw.CharField(null=True)
+ email = pw.CharField(unique=True, null=True)
+ verified = pw.CharField(default=False)
+
+ class Meta:
+ database = db
+
+
+class Content(pw.Model):
+ """
+ Content model is any uploaded content from a creator.
+ """
+ id = pw.AutoField()
+ creator = pw.ForeignKeyField(User)
+ path = pw.CharField()
+ upload_date = pw.DateTimeField(default=datetime.utcnow)
+ last_edit_date = pw.DateTimeField(default=datetime.utcnow)
+ approved = pw.BooleanField(default=False)
+ hidden = pw.BooleanField(default=False)
+ title = pw.CharField()
+ description = pw.TextField()
+
+ class Meta:
+ database = db
+
+
+class Transaction(pw.Model):
+ """
+ Transaction model is a simple reference to a Monero transaction so that we can track
+ which transactions have occurred on-chain and what content they correspond to.
+ """
+ id = pw.AutoField()
+ tx_id = pw.CharField(unique=True)
+ atomic_xmr = pw.BigIntegerField()
+ to_address = pw.CharField()
+ content = pw.ForeignKeyField(Content)
+
+ class Meta:
+ database = db
diff --git a/nerochan/routes/__init__.py b/nerochan/routes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nerochan/routes/api.py b/nerochan/routes/api.py
new file mode 100644
index 0000000..8e1ca0f
--- /dev/null
+++ b/nerochan/routes/api.py
@@ -0,0 +1,11 @@
+from flask import Blueprint, jsonify
+
+
+bp = Blueprint('api', 'api')
+
+@bp.route('/api/test')
+def get_prices():
+ return jsonify({
+ 'test': True,
+ 'message': 'This is only a test.'
+ })
diff --git a/nerochan/routes/auth.py b/nerochan/routes/auth.py
new file mode 100644
index 0000000..51e9e2c
--- /dev/null
+++ b/nerochan/routes/auth.py
@@ -0,0 +1,102 @@
+from flask import Blueprint, render_template
+from flask import flash, redirect, url_for
+from flask_login import login_user, logout_user, current_user
+
+from nerochan.forms import UserLogin, UserRegistration, UserChallenge
+from nerochan.helpers import make_wallet_rpc
+from nerochan.models import User
+
+
+bp = Blueprint('auth', 'auth')
+
+@bp.route("/register", methods=["GET", "POST"])
+def register():
+ form = UserRegistration()
+ if current_user.is_authenticated:
+ flash('Already registered and authenticated.')
+ return redirect(url_for('main.index'))
+
+ if form.validate_on_submit():
+ # Check if handle already exists
+ user = User.select().where(
+ User.handle == form.handle.data
+ ).first()
+ if user:
+ flash('This handle is already registered.')
+ return redirect(url_for('auth.login'))
+
+ # Save new user
+ user = User(
+ handle=form.handle.data,
+ wallet_address=form.wallet_address.data,
+ )
+ user.save()
+ login_user(user)
+ return redirect(url_for('main.index'))
+
+ return render_template("auth/register.html", form=form)
+
+@bp.route("/login", methods=["GET", "POST"])
+def login():
+ form = UserLogin()
+ if current_user.is_authenticated:
+ flash('Already logged in.')
+ return redirect(url_for('main.index'))
+
+ if form.validate_on_submit():
+ # Check if user doesn't exist
+ user = User.select().where(
+ User.handle == form.handle.data
+ ).first()
+ if not user:
+ flash('That handle does not exist.')
+ return redirect(url_for('auth.login'))
+ return redirect(url_for('auth.challenge', handle=user.handle))
+
+ return render_template("auth/login.html", form=form)
+
+@bp.route("/login/challenge/", methods=["GET", "POST"])
+def challenge(handle):
+ form = UserChallenge()
+ user = User.select().where(User.handle == handle).first()
+ if not user:
+ flash('User does not exist.')
+ return redirect(url_for('main.index'))
+
+ if current_user.is_authenticated:
+ flash('Already logged in.')
+ return redirect(url_for('main.index'))
+
+ if form.validate_on_submit():
+ data = {
+ 'data': user.challenge,
+ 'address': user.wallet_address,
+ 'signature': form.signature.data
+ }
+ try:
+ res = make_wallet_rpc('verify', data)
+ if res['good']:
+ user.regenerate_challenge()
+ login_user(user)
+ flash('Successful login!')
+ return redirect(url_for('main.index'))
+ else:
+ flash('Invalid signature. Try again.')
+ return redirect(url_for('auth.challenge', handle=handle))
+ except Exception as e:
+ flash(f'Issue with checking the signature provided: {e}')
+ return redirect(url_for('auth.challenge', handle=handle))
+
+ return render_template(
+ 'auth/challenge.html',
+ user=user,
+ form=form
+ )
+
+@bp.route("/logout")
+def logout():
+ if current_user.is_authenticated:
+ logout_user()
+ else:
+ flash('Not authenticated!')
+ return redirect(url_for('main.index'))
diff --git a/nerochan/routes/main.py b/nerochan/routes/main.py
new file mode 100644
index 0000000..9f9835c
--- /dev/null
+++ b/nerochan/routes/main.py
@@ -0,0 +1,30 @@
+from flask import Blueprint, render_template
+from flask_login import current_user
+
+from nerochan.models import *
+
+
+bp = Blueprint('main', 'main')
+
+@bp.route('/')
+def index():
+ feed = dict()
+ # new_creators = User.select().where(
+ # User.roles.contains_any(UserRole.creator)
+ # ).order_by(User.register_date.desc()).execute()
+ # feed['new_creators'] = new_creators
+ # if current_user.is_authenticated:
+ # active_subscriptions = Subscription.select().where(
+ # Subscription.is_active == True,
+ # Subscription.backer == current_user
+ # ).order_by(Subscription.subscribe_date.desc()).execute()
+ # feed['active_subscriptions'] = active_subscriptions
+ # new_posts = Post.select().where(
+ # Post.hidden == False,
+ # Post.creator in [c.creator for c in active_subscriptions]
+ # ).order_by(Post.post_date.desc()).execute()
+ # feed['new_posts'] = new_posts
+ return render_template(
+ 'index.html',
+ feed=feed
+ )
diff --git a/nerochan/routes/post.py b/nerochan/routes/post.py
new file mode 100644
index 0000000..aa8fbe2
--- /dev/null
+++ b/nerochan/routes/post.py
@@ -0,0 +1,27 @@
+from flask import Blueprint, render_template, flash, redirect, url_for
+from flask_login import current_user
+
+# from nerochan.models import Content, User
+
+
+bp = Blueprint('post', 'post')
+
+# @bp.route('/post/')
+# def show(post_id):
+# post = TextPost.get_or_none(post_id)
+# if post:
+# if current_user.is_anonymous:
+# flash('You must login to view this post.')
+# return redirect(url_for('creator.show', username=post.creator.user.username))
+# user_subscriptions = Subscription.select().where(
+# Subscription.active == True,
+# Subscription.backer == current_user.backer_profile.first()
+# )
+# if user_subscriptions:
+# return render_template('post/show.html', post=post)
+# else:
+# flash('Viewing posts requires a subscription.')
+# return redirect(url_for('creator.subscription', username=post.creator.user.username))
+# else:
+# flash('That post does not exist.')
+# return redirect(url_for('meta.index'))
diff --git a/nerochan/static/css/main.css b/nerochan/static/css/main.css
new file mode 100644
index 0000000..e69de29
diff --git a/nerochan/static/css/noty-relax.css b/nerochan/static/css/noty-relax.css
new file mode 100644
index 0000000..f5f99ec
--- /dev/null
+++ b/nerochan/static/css/noty-relax.css
@@ -0,0 +1,46 @@
+.noty_theme__relax.noty_bar {
+ margin: 4px 0;
+ overflow: hidden;
+ border-radius: 2px;
+ position: relative; }
+ .noty_theme__relax.noty_bar .noty_body {
+ padding: 10px; }
+ .noty_theme__relax.noty_bar .noty_buttons {
+ border-top: 1px solid #e7e7e7;
+ padding: 5px 10px; }
+
+.noty_theme__relax.noty_type__alert,
+.noty_theme__relax.noty_type__notification {
+ background-color: #fff;
+ border: 1px solid #dedede;
+ color: #444; }
+
+.noty_theme__relax.noty_type__warning {
+ background-color: #FFEAA8;
+ border: 1px solid #FFC237;
+ color: #826200; }
+ .noty_theme__relax.noty_type__warning .noty_buttons {
+ border-color: #dfaa30; }
+
+.noty_theme__relax.noty_type__error {
+ background-color: #FF8181;
+ border: 1px solid #e25353;
+ color: #FFF; }
+ .noty_theme__relax.noty_type__error .noty_buttons {
+ border-color: darkred; }
+
+.noty_theme__relax.noty_type__info,
+.noty_theme__relax.noty_type__information {
+ background-color: #78C5E7;
+ border: 1px solid #3badd6;
+ color: #FFF; }
+ .noty_theme__relax.noty_type__info .noty_buttons,
+ .noty_theme__relax.noty_type__information .noty_buttons {
+ border-color: #0B90C4; }
+
+.noty_theme__relax.noty_type__success {
+ background-color: #BCF5BC;
+ border: 1px solid #7cdd77;
+ color: darkgreen; }
+ .noty_theme__relax.noty_type__success .noty_buttons {
+ border-color: #50C24E; }
diff --git a/nerochan/static/css/noty.css b/nerochan/static/css/noty.css
new file mode 100644
index 0000000..ee33b8f
--- /dev/null
+++ b/nerochan/static/css/noty.css
@@ -0,0 +1,222 @@
+.noty_layout_mixin, #noty_layout__top, #noty_layout__topLeft, #noty_layout__topCenter, #noty_layout__topRight, #noty_layout__bottom, #noty_layout__bottomLeft, #noty_layout__bottomCenter, #noty_layout__bottomRight, #noty_layout__center, #noty_layout__centerLeft, #noty_layout__centerRight {
+ position: fixed;
+ margin: 0;
+ padding: 0;
+ z-index: 9999999;
+ -webkit-transform: translateZ(0) scale(1, 1);
+ transform: translateZ(0) scale(1, 1);
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ -webkit-font-smoothing: subpixel-antialiased;
+ filter: blur(0);
+ -webkit-filter: blur(0);
+ max-width: 90%; }
+
+#noty_layout__top {
+ top: 0;
+ left: 5%;
+ width: 90%; }
+
+#noty_layout__topLeft {
+ top: 20px;
+ left: 20px;
+ width: 325px; }
+
+#noty_layout__topCenter {
+ top: 5%;
+ left: 50%;
+ width: 325px;
+ -webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
+ transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
+
+#noty_layout__topRight {
+ top: 20px;
+ right: 20px;
+ width: 325px; }
+
+#noty_layout__bottom {
+ bottom: 0;
+ left: 5%;
+ width: 90%; }
+
+#noty_layout__bottomLeft {
+ bottom: 20px;
+ left: 20px;
+ width: 325px; }
+
+#noty_layout__bottomCenter {
+ bottom: 5%;
+ left: 50%;
+ width: 325px;
+ -webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
+ transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
+
+#noty_layout__bottomRight {
+ bottom: 20px;
+ right: 20px;
+ width: 325px; }
+
+#noty_layout__center {
+ top: 50%;
+ left: 50%;
+ width: 325px;
+ -webkit-transform: translate(-webkit-calc(-50% - .5px), -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
+ transform: translate(calc(-50% - .5px), calc(-50% - .5px)) translateZ(0) scale(1, 1); }
+
+#noty_layout__centerLeft {
+ top: 50%;
+ left: 20px;
+ width: 325px;
+ -webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
+ transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
+
+#noty_layout__centerRight {
+ top: 50%;
+ right: 20px;
+ width: 325px;
+ -webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
+ transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
+
+.noty_progressbar {
+ display: none; }
+
+.noty_has_timeout.noty_has_progressbar .noty_progressbar {
+ display: block;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ height: 3px;
+ width: 100%;
+ background-color: #646464;
+ opacity: 0.2;
+ filter: alpha(opacity=10); }
+
+.noty_bar {
+ -webkit-backface-visibility: hidden;
+ -webkit-transform: translate(0, 0) translateZ(0) scale(1, 1);
+ -ms-transform: translate(0, 0) scale(1, 1);
+ transform: translate(0, 0) scale(1, 1);
+ -webkit-font-smoothing: subpixel-antialiased;
+ overflow: hidden; }
+
+.noty_effects_open {
+ opacity: 0;
+ -webkit-transform: translate(50%);
+ -ms-transform: translate(50%);
+ transform: translate(50%);
+ -webkit-animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ -webkit-animation-fill-mode: forwards;
+ animation-fill-mode: forwards; }
+
+.noty_effects_close {
+ -webkit-animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ -webkit-animation-fill-mode: forwards;
+ animation-fill-mode: forwards; }
+
+.noty_fix_effects_height {
+ -webkit-animation: noty_anim_height 75ms ease-out;
+ animation: noty_anim_height 75ms ease-out; }
+
+.noty_close_with_click {
+ cursor: pointer; }
+
+.noty_close_button {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ font-weight: bold;
+ width: 20px;
+ height: 20px;
+ text-align: center;
+ line-height: 20px;
+ background-color: rgba(0, 0, 0, 0.05);
+ border-radius: 2px;
+ cursor: pointer;
+ -webkit-transition: all .2s ease-out;
+ transition: all .2s ease-out; }
+
+.noty_close_button:hover {
+ background-color: rgba(0, 0, 0, 0.1); }
+
+.noty_modal {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ background-color: #000;
+ z-index: 10000;
+ opacity: .3;
+ left: 0;
+ top: 0; }
+
+.noty_modal.noty_modal_open {
+ opacity: 0;
+ -webkit-animation: noty_modal_in .3s ease-out;
+ animation: noty_modal_in .3s ease-out; }
+
+.noty_modal.noty_modal_close {
+ -webkit-animation: noty_modal_out .3s ease-out;
+ animation: noty_modal_out .3s ease-out;
+ -webkit-animation-fill-mode: forwards;
+ animation-fill-mode: forwards; }
+
+@-webkit-keyframes noty_modal_in {
+ 100% {
+ opacity: .3; } }
+
+@keyframes noty_modal_in {
+ 100% {
+ opacity: .3; } }
+
+@-webkit-keyframes noty_modal_out {
+ 100% {
+ opacity: 0; } }
+
+@keyframes noty_modal_out {
+ 100% {
+ opacity: 0; } }
+
+@keyframes noty_modal_out {
+ 100% {
+ opacity: 0; } }
+
+@-webkit-keyframes noty_anim_in {
+ 100% {
+ -webkit-transform: translate(0);
+ transform: translate(0);
+ opacity: 1; } }
+
+@keyframes noty_anim_in {
+ 100% {
+ -webkit-transform: translate(0);
+ transform: translate(0);
+ opacity: 1; } }
+
+@-webkit-keyframes noty_anim_out {
+ 100% {
+ -webkit-transform: translate(50%);
+ transform: translate(50%);
+ opacity: 0; } }
+
+@keyframes noty_anim_out {
+ 100% {
+ -webkit-transform: translate(50%);
+ transform: translate(50%);
+ opacity: 0; } }
+
+@-webkit-keyframes noty_anim_height {
+ 100% {
+ height: 0; } }
+
+@keyframes noty_anim_height {
+ 100% {
+ height: 0; } }
+
+/*# sourceMappingURL=noty.css.map*/
+
+
+/* Custom */
+.noty_body {
+ text-align: center;
+}
diff --git a/nerochan/static/css/noty.css.map b/nerochan/static/css/noty.css.map
new file mode 100644
index 0000000..70e0c46
--- /dev/null
+++ b/nerochan/static/css/noty.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":[],"names":[],"mappings":"","file":"noty.css","sourceRoot":""}
\ No newline at end of file
diff --git a/nerochan/static/images/monero-logo.png b/nerochan/static/images/monero-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..bf7ab3261b16e36f6ca9dbfa07290f3b2d4b5d5b
GIT binary patch
literal 11912
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
literal 0
HcmV?d00001
diff --git a/nerochan/templates/auth/challenge.html b/nerochan/templates/auth/challenge.html
new file mode 100644
index 0000000..aa3468b
--- /dev/null
+++ b/nerochan/templates/auth/challenge.html
@@ -0,0 +1,11 @@
+{% extends 'includes/base.html' %}
+
+{% block content %}
+
+Challenge
+Handle: {{ user.handle }}
+Challenge: {{ user.challenge }}
+Wallet Address: {{ user.wallet_address }}
+{% include 'includes/form.html' %}
+
+{% endblock %}
diff --git a/nerochan/templates/auth/login.html b/nerochan/templates/auth/login.html
new file mode 100644
index 0000000..a962ae5
--- /dev/null
+++ b/nerochan/templates/auth/login.html
@@ -0,0 +1,8 @@
+{% extends 'includes/base.html' %}
+
+{% block content %}
+
+Login
+{% include 'includes/form.html' %}
+
+{% endblock %}
diff --git a/nerochan/templates/auth/register.html b/nerochan/templates/auth/register.html
new file mode 100644
index 0000000..f68216c
--- /dev/null
+++ b/nerochan/templates/auth/register.html
@@ -0,0 +1,8 @@
+{% extends 'includes/base.html' %}
+
+{% block content %}
+
+Register
+{% include 'includes/form.html' %}
+
+{% endblock %}
diff --git a/nerochan/templates/includes/base.html b/nerochan/templates/includes/base.html
new file mode 100644
index 0000000..85fa9c4
--- /dev/null
+++ b/nerochan/templates/includes/base.html
@@ -0,0 +1,18 @@
+
+
+ {% include 'includes/head.html' %}
+
+ {% include 'includes/header.html' %}
+ {% block content %}{% endblock %}
+ {% include 'includes/debug.html' %}
+
+
+
diff --git a/nerochan/templates/includes/debug.html b/nerochan/templates/includes/debug.html
new file mode 100644
index 0000000..812cf15
--- /dev/null
+++ b/nerochan/templates/includes/debug.html
@@ -0,0 +1,9 @@
+{% if config.DEBUG and current_user.is_authenticated %}
+
+ Debug
+
+ Username: {{ current_user.handle }}
+ {% if current_user.email %}Email: {{ current_user.email }}
{% endif %}
+ Wallet Address: {{ current_user.wallet_address }}
+
+{% endif %}
diff --git a/nerochan/templates/includes/footer.html b/nerochan/templates/includes/footer.html
new file mode 100644
index 0000000..bfaf3f5
--- /dev/null
+++ b/nerochan/templates/includes/footer.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/nerochan/templates/includes/form.html b/nerochan/templates/includes/form.html
new file mode 100644
index 0000000..98f3518
--- /dev/null
+++ b/nerochan/templates/includes/form.html
@@ -0,0 +1,18 @@
+
diff --git a/nerochan/templates/includes/head.html b/nerochan/templates/includes/head.html
new file mode 100644
index 0000000..e8068a8
--- /dev/null
+++ b/nerochan/templates/includes/head.html
@@ -0,0 +1,22 @@
+
+ {{ config.SITE_NAME }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nerochan/templates/includes/header.html b/nerochan/templates/includes/header.html
new file mode 100644
index 0000000..7cacdbc
--- /dev/null
+++ b/nerochan/templates/includes/header.html
@@ -0,0 +1,20 @@
+
+
+{% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+
+ {% for category, message in messages %}
+
{{ message }} - {{ category }}
+ {% endfor %}
+
+ {% endif %}
+{% endwith %}
diff --git a/nerochan/templates/index.html b/nerochan/templates/index.html
new file mode 100644
index 0000000..55f06cc
--- /dev/null
+++ b/nerochan/templates/index.html
@@ -0,0 +1,9 @@
+{% extends 'includes/base.html' %}
+
+{% block content %}
+
+{% if current_user.is_authenticated %}
+ logged in: {{ current_user.handle }}
+{% endif %}
+
+{% endblock %}
diff --git a/nerochan/templates/post/show.html b/nerochan/templates/post/show.html
new file mode 100644
index 0000000..80d20b4
--- /dev/null
+++ b/nerochan/templates/post/show.html
@@ -0,0 +1,25 @@
+
+
+
+ {% include 'includes/head.html' %}
+
+
+
+
+ {% include 'includes/header.html' %}
+
+ {% if post %}
+
{{ post.title }}
+
Posted: {{ post.post_date }}
+
Edited: {{ post.last_edit_date }}
+
{{ post.content }}
+ {% endif %}
+
+ {% include 'includes/footer.html' %}
+
+
+
+ {% include 'includes/scripts.html' %}
+
+
+
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2dcc89c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,17 @@
+Flask-WTF
+flask-login
+flask-bcrypt
+hypercorn
+Pillow
+psycopg2-binary
+python-dotenv
+qrcode
+redis
+peewee
+requests
+WTForms
+flask-session
+flask
+monero
+arrow
+flake8