diff --git a/Makefile b/Makefile index 6b843c7..7b7dfb4 100644 --- a/Makefile +++ b/Makefile @@ -20,4 +20,4 @@ shell: ## Start Quart CLI shell QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=0 QUART_ENV=production .venv/bin/quart shell dev: ## Start Quart development web server - QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=1 QUART_ENV=development .venv/bin/quart run + QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=1 QUART_ENV=development QUART_RELOAD=true .venv/bin/quart run diff --git a/README.md b/README.md index 486c901..0da16a5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ -# lza-quart-app -Template project for Quart (Python/Flask) applications. +# xmrbackers + +That's the name for now. The intention is to provide a secure, although centralized (for now) web service for hosting private content and allowing subscribers to view with subscriptions paid in Monero (XMR). + +It's intended to provide any sort of digital content experience - text, images, videos, streams, and have all the robust crypto mechanics for authenticating and validating transactions. + diff --git a/app.py b/app.py old mode 100644 new mode 100755 index ead8c71..5384054 --- a/app.py +++ b/app.py @@ -1,8 +1,16 @@ -from xmrbackers.factory import create_app +#!/usr/bin/env python3 + +from os import environ + +from xmrbackers import create_app app = create_app() if __name__ == '__main__': + # print('running') + # environ['QUART_SECRETS'] = 'config.py' + # environ['QUART_DEBUG'] = '1' + # environ['QUART_ENV'] = 'development' app.run(debug=True, use_reloader=True) diff --git a/env-example b/env-example index f63caf8..9538deb 100644 --- a/env-example +++ b/env-example @@ -3,6 +3,8 @@ DB_USER=xmrbackers DB_NAME=xmrbackers DB_HOST=localhost +PLATFORM_WALLET=xxxxxx + XMR_WALLET_PATH=/data/xmr-wallet XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx XMR_WALLET_RPC_USER=xxxxxxxxxx diff --git a/xmrbackers/config.py b/xmrbackers/config.py index 2cfb37f..e091f06 100644 --- a/xmrbackers/config.py +++ b/xmrbackers/config.py @@ -1,8 +1,9 @@ -from dotenv import load_dotenv from secrets import token_urlsafe from datetime import timedelta from os import getenv +from dotenv import load_dotenv + load_dotenv() @@ -11,16 +12,21 @@ SITE_NAME = getenv('SITE_NAME', 'xmrbackers') SECRET_KEY = getenv('SECRET_KEY') STATS_TOKEN = getenv('STATS_TOKEN', token_urlsafe(8)) SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000') +PLATFORM_WALLET = getenv('PLATFORM_WALLET') + +# Constants - here for easy template references +CREATOR_SUBSCRIPTION_TERM = 60 # term of how long creator subscriptions are valid for +CREATOR_SUBSCRIPTION_GRACE = 10 # grace period after expiration of creator subscriptions until content is hidden +CREATOR_SUBSCRIPTION_DELETE = 20 # time after grace period until content is deleted +CREATOR_SUBSCRIPTION_FEE_XMR = .15 # default flat rate fee in XMR for creator subscriptions # 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_DAEMON_URI = getenv('XMR_DAEMON_URI') XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK') - # Database DB_HOST = getenv('DB_HOST', 'localhost') DB_PORT = getenv('DB_PORT', 5432) diff --git a/xmrbackers/forms.py b/xmrbackers/forms.py index 01e51a4..4beae00 100644 --- a/xmrbackers/forms.py +++ b/xmrbackers/forms.py @@ -36,3 +36,8 @@ class ConfirmSubscription(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"}) wallet_address = StringField('XMR Address:', validators=[DataRequired()], render_kw={"placeholder": "XMR Address", "class": "form-control", "type": "text"}) + + +class ConfirmCreatorSubscription(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"}) \ No newline at end of file diff --git a/xmrbackers/models.py b/xmrbackers/models.py index 2b56c64..5be8ae7 100644 --- a/xmrbackers/models.py +++ b/xmrbackers/models.py @@ -89,10 +89,42 @@ class Profile(pw.Model): 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 which subscription they correspond to. + """ + id = pw.AutoField() + tx_id = pw.CharField(unique=True) + atomic_xmr = pw.BigIntegerField() + + class Meta: + database = db + + +class CreatorSubscription(pw.Model): + """ + CreatorSubscription model is for tracking subscriptions of creators to the platform. + The first subscription is a flat fee, the following will be based on usage/consumption + and will be re-negotiated every N days. + """ + id = pw.AutoField() + user = pw.ForeignKeyField(User) + tx = pw.ForeignKeyField(Transaction) + create_date = pw.DateTimeField(default=datetime.utcnow) + atomic_xmr = pw.BigIntegerField() + term_hours = pw.IntegerField(default=config.CREATOR_SUBSCRIPTION_TERM * 24) + grace_hours = pw.IntegerField(default=config.CREATOR_SUBSCRIPTION_GRACE * 24) + delete_hours = pw.IntegerField(default=config.CREATOR_SUBSCRIPTION_DELETE * 24) + + class Meta: + database = db + + class SubscriptionMeta(pw.Model): """ SubscriptionMeta model is for the Creator to define details about - their subscription plan to release for subscribers. There is no + their subscription plan to release for subscribers/backers. There is no editing in place, only creating new plans; anyone utilizing an existing subscription (by loading it on screen) will be grandfathered in. """ @@ -120,6 +152,7 @@ class Subscription(pw.Model): """ id = pw.AutoField() subscribe_date = pw.DateTimeField(default=datetime.utcnow) + tx = pw.ForeignKeyField(Transaction) active = pw.BooleanField(default=True) creator = pw.ForeignKeyField(User) backer = pw.ForeignKeyField(User) diff --git a/xmrbackers/routes/creator.py b/xmrbackers/routes/creator.py index 92f328f..3b1ed76 100644 --- a/xmrbackers/routes/creator.py +++ b/xmrbackers/routes/creator.py @@ -2,11 +2,10 @@ from quart import Blueprint, render_template, flash, redirect, url_for from flask_login import current_user, login_required from monero.wallet import Wallet -from xmrbackers.forms import ConfirmSubscription from xmrbackers.models import User, Profile, Content from xmrbackers.models import Subscription, SubscriptionMeta, UserRole from xmrbackers.helpers import check_tx_key -from xmrbackers import config +from xmrbackers import config, forms bp = Blueprint('creator', 'creator') @@ -20,6 +19,26 @@ async def all(): ) return await render_template('creator/creators.html', creators=creators) +@bp.route('/creators/join') +@login_required +async def join(): + form = forms.ConfirmCreatorSubscription() + + if UserRole.creator in current_user.roles: + await flash('You already are a creator!', 'warning') + + if not config.PLATFORM_WALLET: + await flash('Platform operator has not setup wallet yet. Try later.', 'warning') + + return render_template( + 'creator/join.html', + new_fee_xmr=config.CREATOR_SUBSCRIPTION_FEE_XMR, + form=form + ) + + return redirect(url_for('main.index')) + + @bp.route('/creator/') async def show(handle): creator = User.select().where(User.handle == handle, User.roles.contains_any(UserRole.creator)) diff --git a/xmrbackers/templates/creator/join.html b/xmrbackers/templates/creator/join.html new file mode 100644 index 0000000..1df135a --- /dev/null +++ b/xmrbackers/templates/creator/join.html @@ -0,0 +1,30 @@ +{% extends 'includes/base.html' %} + +{% block content %} +

Become a Creator

+

+ First time creators will pay a flat rate of {{ new_fee_xmr }} XMR.
+ Please send fees to the platform wallet below and provide your tx_hash and tx_key in the form below to initiate your platform subscription. +

+

Platform Wallet: {{ config.PLATFORM_WALLET }}

+ +
+ {% for f in form %} + {% if f.name == 'csrf_token' %} + {{ f }} + {% else %} +
+ {{ f.label }} + {{ f }} +
+ {% endif %} + {% endfor %} + + +
+ +{% endblock %} \ No newline at end of file diff --git a/xmrbackers/templates/includes/base.html b/xmrbackers/templates/includes/base.html new file mode 100644 index 0000000..4959b1d --- /dev/null +++ b/xmrbackers/templates/includes/base.html @@ -0,0 +1,10 @@ + + + {% include 'includes/head.html' %} + + {% include 'includes/header.html' %} + {% block content %}{% endblock %} + {% include 'includes/footer.html' %} + {% include 'includes/scripts.html' %} + + diff --git a/xmrbackers/templates/includes/header.html b/xmrbackers/templates/includes/header.html index ec7f4a3..cf52208 100644 --- a/xmrbackers/templates/includes/header.html +++ b/xmrbackers/templates/includes/header.html @@ -30,7 +30,7 @@ new Noty({ type: '{{ _c }}', theme: 'relax', - layout: 'topRight', + layout: 'topCenter', text: '{{ message }}', timeout: 4500 }).show(); diff --git a/xmrbackers/templates/index.html b/xmrbackers/templates/index.html index c868941..6842a3d 100644 --- a/xmrbackers/templates/index.html +++ b/xmrbackers/templates/index.html @@ -8,6 +8,12 @@ {% include 'includes/header.html' %} + {% if current_user.is_authenticated %} + {% if 2 not in current_user.roles %} + Become a Creator + {% endif %} + {% endif %} + {% if feed %} {% if feed['new_creators'] %}

New Creators