getting basic platform subscriptions working

revamp
lza_menace 2 years ago
parent 5721eaf054
commit 856490cf50

@ -10,12 +10,18 @@ setup: ## Establish local environment with dependencies installed
python3 -m venv .venv python3 -m venv .venv
.venv/bin/pip install -r requirements.txt .venv/bin/pip install -r requirements.txt
build: ## Build containers
docker-compose build
up: ## Start containers up: ## Start containers
docker-compose up -d docker-compose up -d
down: ## Stop containers down: ## Stop containers
docker-compose down docker-compose down
dbshell:
docker-compose exec db psql -U xmrbackers
shell: ## Start Quart CLI shell shell: ## Start Quart CLI shell
QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=0 QUART_ENV=production .venv/bin/quart shell QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=0 QUART_ENV=production .venv/bin/quart shell

@ -5,8 +5,6 @@ DB_HOST=localhost
PLATFORM_WALLET=xxxxxx PLATFORM_WALLET=xxxxxx
XMR_WALLET_PATH=/data/xmr-wallet
XMR_WALLET_PASS=xxxxxxxxxxxxxxxxxxx
XMR_WALLET_RPC_USER=xxxxxxxxxx XMR_WALLET_RPC_USER=xxxxxxxxxx
XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx
XMR_WALLET_RPC_PORT=8000 XMR_WALLET_RPC_PORT=8000

@ -9,7 +9,6 @@ qrcode
redis redis
peewee peewee
requests requests
SQLAlchemy
WTForms WTForms
quart==0.17.0 quart==0.17.0
monero monero

@ -35,9 +35,3 @@ class UserChallenge(FlaskForm):
class ConfirmSubscription(FlaskForm): class ConfirmSubscription(FlaskForm):
tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={"placeholder": "TX ID", "class": "form-control", "type": "text"}) 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"}) 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"})

@ -6,26 +6,26 @@ from monero.wallet import Wallet
from xmrbackers import config from xmrbackers import config
def check_tx_key(tx_id, tx_key, wallet_address): # def check_tx_key(tx_id, tx_key, wallet_address):
try: # try:
check_data = { # check_data = {
'txid': tx_id, # 'txid': tx_id,
'tx_key': tx_key, # 'tx_key': tx_key,
'address': wallet_address # 'address': wallet_address
} # }
w = Wallet(port=config.XMR_WALLET_RPC_PORT, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS) # w = Wallet(port=config.XMR_WALLET_RPC_PORT, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS)
res = w._backend.raw_request('check_tx_key', check_data) # res = w._backend.raw_request('check_tx_key', check_data)
return res # return res
except: # except:
raise Exception('there was a problem i dont feel like writing good code for right now') # raise Exception('there was a problem i dont feel like writing good code for right now')
def make_wallet_rpc(method, data={}): def make_wallet_rpc(method, data={}):
try: try:
w = Wallet( w = Wallet(
port=config.XMR_WALLET_RPC_PORT, port=config.XMR_WALLET_RPC_PORT,
user=config.XMR_WALLET_RPC_USER, user=config.XMR_WALLET_RPC_USER,
password=config.XMR_WALLET_RPC_PASS, password=config.XMR_WALLET_RPC_PASS,
timeout=5 timeout=3
) )
res = w._backend.raw_request(method, data) res = w._backend.raw_request(method, data)
return res return res
@ -101,4 +101,4 @@ class EnumIntField(pw.IntegerField):
return value.value return value.value
def python_value(self, value): def python_value(self, value):
return self.enum_class(value) return self.enum_class(value)

@ -97,6 +97,7 @@ class Transaction(pw.Model):
id = pw.AutoField() id = pw.AutoField()
tx_id = pw.CharField(unique=True) tx_id = pw.CharField(unique=True)
atomic_xmr = pw.BigIntegerField() atomic_xmr = pw.BigIntegerField()
to_address = pw.CharField()
class Meta: class Meta:
database = db database = db
@ -104,7 +105,7 @@ class Transaction(pw.Model):
class CreatorSubscription(pw.Model): class CreatorSubscription(pw.Model):
""" """
CreatorSubscription model is for tracking subscriptions of creators to the platform. 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 The first subscription is a flat fee, the following will be based on usage/consumption
and will be re-negotiated every N days. and will be re-negotiated every N days.
""" """
@ -146,8 +147,8 @@ class SubscriptionMeta(pw.Model):
class Subscription(pw.Model): class Subscription(pw.Model):
""" """
Subscription model gets created when users/backers can confirm payment via Subscription model gets created when users/backers can confirm payment via
the `check_tx_key` RPC method to the creator's wallet. Once a subscription the `check_tx_key` RPC method to the creator's wallet. Once a subscription
is in place and is associated with a user, that user is then elligible to is in place and is associated with a user, that user is then elligible to
view that creator's content until the subscription expires (number_hours). view that creator's content until the subscription expires (number_hours).
""" """
id = pw.AutoField() id = pw.AutoField()
@ -164,7 +165,7 @@ class Subscription(pw.Model):
class Content(pw.Model): class Content(pw.Model):
""" """
Content model represents any uploaded content from a creator which is only Content model represents any uploaded content from a creator which is only
viewable by backers with an active subscription. viewable by backers with an active subscription.
""" """
id = pw.AutoField() id = pw.AutoField()

@ -1,10 +1,11 @@
from quart import Blueprint, render_template, flash, redirect, url_for from quart import Blueprint, render_template, flash, redirect, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from monero.wallet import Wallet from monero.wallet import Wallet
from monero.address import address
from monero.numbers import to_atomic, from_atomic
from xmrbackers.models import User, Profile, Content from xmrbackers.models import *
from xmrbackers.models import Subscription, SubscriptionMeta, UserRole from xmrbackers.helpers import make_wallet_rpc
from xmrbackers.helpers import check_tx_key
from xmrbackers import config, forms from xmrbackers import config, forms
@ -19,42 +20,107 @@ async def all():
) )
return await render_template('creator/creators.html', creators=creators) return await render_template('creator/creators.html', creators=creators)
@bp.route('/creators/join') @bp.route('/creators/join', methods=['GET', 'POST'])
@login_required @login_required
async def join(): async def join():
form = forms.ConfirmCreatorSubscription() form = forms.ConfirmSubscription()
valid_address = False
try:
address(config.PLATFORM_WALLET)
valid_address = True
except:
pass
if not config.PLATFORM_WALLET or valid_address is False:
await flash('Platform operator has not setup wallet yet. Try later.', 'warning')
return redirect(url_for('main.index'))
if UserRole.creator in current_user.roles: if UserRole.creator in current_user.roles:
await flash('You already are a creator!', 'warning') await flash('You already are a creator!', 'warning')
return redirect(url_for('main.index'))
if not config.PLATFORM_WALLET:
await flash('Platform operator has not setup wallet yet. Try later.', 'warning') if form.validate_on_submit():
try:
return render_template( data = {
'creator/join.html', 'txid': form.tx_id.data,
'tx_key': form.tx_key.data,
'address': config.PLATFORM_WALLET
}
res = make_wallet_rpc('check_tx_key', data)
existing_tx = Transaction.select().where(
Transaction.tx_id == form.tx_id.data.lower()
).first()
existing_sub = CreatorSubscription.select().where(
CreatorSubscription.tx == existing_tx
).first()
for i in ['confirmations', 'in_pool', 'received']:
assert i in res
if res['in_pool']:
await flash('That transaction is still in the mempool. You need to wait a few more minutes for it to clear. Try again in a bit.', 'warning')
elif res['received'] < to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR):
await flash(f'Not enought XMR sent. {from_atomic(res["received"])} XMR sent, expected {config.CREATOR_SUBSCRIPTION_FEE_XMR} XMR.', 'error')
elif existing_tx:
if existing_sub:
await flash('This transaction was already used for another subscription.', 'warning')
else:
await flash('Adding creator subscription.', 'success')
c = CreatorSubscription(
user=current_user,
tx=existing_tx,
atomic_xmr=res['received']
)
c.save()
current_user.roles.append(UserRole.creator)
current_user.save()
elif res['received'] >= to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR):
await flash('Success! Welcome to the creator club!', 'success')
t = Transaction(
tx_id=form.tx_id.data.lower(),
atomic_xmr=res['received'],
to_address=config.PLATFORM_WALLET
)
t.save()
c = CreatorSubscription(
user=current_user,
tx=t,
atomic_xmr=res['received']
)
c.save()
current_user.roles.append(UserRole.creator)
current_user.save()
return redirect(url_for('creator.show', handle=current_user.handle))
else:
await flash('Something went wrong. No idea what, though. Check with admin.', 'error')
except Exception as e:
await flash(f'seems bad: {e}', 'error')
return await render_template(
'creator/join.html',
new_fee_xmr=config.CREATOR_SUBSCRIPTION_FEE_XMR, new_fee_xmr=config.CREATOR_SUBSCRIPTION_FEE_XMR,
form=form form=form
) )
return redirect(url_for('main.index'))
@bp.route('/creator/<handle>') @bp.route('/creator/<handle>')
async def show(handle): async def show(handle):
creator = User.select().where(User.handle == handle, User.roles.contains_any(UserRole.creator)) creator = User.select().where(
if creator: User.handle == handle,
posts = Content.select().where( User.roles.contains_any(UserRole.creator)
Content.creator == creator, )
Content.hidden == False if not creator:
).order_by(Content.post_date.desc())
return await render_template(
'creator/creator.html',
creator=creator,
posts=posts
)
else:
await flash('That creator does not exist.') await flash('That creator does not exist.')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
posts = Content.select().where(
Content.creator == creator,
Content.hidden == False
).order_by(Content.post_date.desc())
return await render_template(
'creator/show.html',
creator=creator,
posts=posts
)
# @bp.route('/creator/<username>/subscription') # @bp.route('/creator/<username>/subscription')
# async def subscription(username): # async def subscription(username):

@ -1,30 +0,0 @@
<!DOCTYPE HTML>
<html>
{% include 'includes/head.html' %}
<body class="is-preload landing">
<div id="page-wrapper">
{% include 'includes/header.html' %}
{% if creator %}
<h1>{{ creator.user.username }}</h1>
<p>Bio: {{ creator.bio }}</p>
{% endif %}
{% if posts %}
<h1>Posts</h1>
{% for post in posts %}
<p><a href="{{ url_for('post.show', post_id=post.id) }}">{{ post.id }} - {{ post.title }} - {{ post.post_date | humanize }}</a></p>
{% endfor %}
{% endif %}
{% include 'includes/footer.html' %}
</div>
{% include 'includes/scripts.html' %}
</body>
</html>

@ -27,4 +27,4 @@
<input type="submit" value="Confirm" class="btn btn-link btn-outline btn-xl"> <input type="submit" value="Confirm" class="btn btn-link btn-outline btn-xl">
</form> </form>
{% endblock %} {% endblock %}

@ -0,0 +1,15 @@
{% extends 'includes/base.html' %}
{% block content %}
<h1>{{ creator.handle }}</h1>
<p>Bio: {{ creator.bio }}</p>
{% if posts %}
<h1>Posts</h1>
{% for post in posts %}
<p><a href="{{ url_for('post.show', post_id=post.id) }}">{{ post.id }} - {{ post.title }} - {{ post.post_date | humanize }}</a></p>
{% endfor %}
{% endif %}
{% endblock %}
Loading…
Cancel
Save