Compare commits
9 Commits
Author | SHA1 | Date |
---|---|---|
lza_menace | ad6bad9b5e | 2 years ago |
lza_menace | 680dc5aff5 | 2 years ago |
lza_menace | 3e087646f5 | 2 years ago |
lza_menace | 25c16e5e72 | 2 years ago |
lza_menace | 3da8c9951d | 2 years ago |
lza_menace | 856490cf50 | 2 years ago |
lza_menace | 5721eaf054 | 2 years ago |
lza_menace | 6681315128 | 2 years ago |
lza_menace | 6dc1b351b8 | 2 years ago |
@ -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.
|
||||
|
||||
|
@ -1,7 +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__':
|
||||
app.run()
|
||||
# print('running')
|
||||
# environ['QUART_SECRETS'] = 'config.py'
|
||||
# environ['QUART_DEBUG'] = '1'
|
||||
# environ['QUART_ENV'] = 'development'
|
||||
app.run(debug=True, use_reloader=True)
|
||||
|
@ -0,0 +1,16 @@
|
||||
# Platform Sub
|
||||
|
||||
### Good one
|
||||
tx_id: c50b4c2c2ca26691ac6205c67d72052a882aa0d71d71b18bdfe0dd887d8b75aa
|
||||
tx_key: aaf3d3b81e57b2c874f0428a50428cf38ba5369c2ad06df7d4d7e7e7bb43e308
|
||||
|
||||
### Bad one
|
||||
tx_id: 7fd5111343a0cc9c44705a03cddb4115027a2671855806f0a0dffddc7cc889ab
|
||||
tx_key: c7f1c8266a6109ea547c7c5d04d226f68faa5f5a4cf1eb7ad6077cdb2bbb830e
|
||||
|
||||
|
||||
# Content Sub
|
||||
|
||||
tx_id: 1127feb964a2f35ea819454fc8dd17f23f374f829364704a7a51cbec676e09a5
|
||||
tx_key: 12ca27135e6dfc1bb5f2b6dc6334a84e3e9c09003db4204061a553a49452f505
|
||||
address: 75gXGUPpyqb1Hhxv6wSZ9218x1QBjBNo3dvYX6FnVMRPCWXoAX52DZnCkAe9LwjRjaGB1A6CGUr5c5hUTqdqywxN9jsutNP
|
@ -0,0 +1 @@
|
||||
from xmrbackers.factory import create_app
|
@ -1,14 +1,46 @@
|
||||
# import quart.flask_patch
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField
|
||||
from wtforms.validators import DataRequired
|
||||
from wtforms import StringField, FloatField
|
||||
from wtforms.validators import DataRequired, ValidationError
|
||||
from monero.address import address
|
||||
|
||||
from xmrbackers import config
|
||||
|
||||
class UserAuth(FlaskForm):
|
||||
username = StringField('Username:', validators=[DataRequired()], render_kw={"placeholder": "Username", "class": "form-control", "type": "text"})
|
||||
password = StringField('Password:', validators=[DataRequired()], render_kw={"placeholder": "Password", "class": "form-control", "type": "password"})
|
||||
|
||||
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"})
|
||||
def is_valid_xmr_address(form, field):
|
||||
try:
|
||||
# Ensure the provided address is valid address/subaddress/integrated address
|
||||
a = address(field.data)
|
||||
# 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'})
|
||||
|
@ -1,17 +1,90 @@
|
||||
import peewee as pw
|
||||
import playhouse.postgres_ext as pwpg
|
||||
from monero.wallet import Wallet
|
||||
from monero.exceptions import WrongAddress
|
||||
|
||||
from xmrbackers import config
|
||||
|
||||
def check_tx_key(tx_id, tx_key, wallet_address):
|
||||
|
||||
def make_wallet_rpc(method, data={}):
|
||||
try:
|
||||
check_data = {
|
||||
'txid': tx_id,
|
||||
'tx_key': tx_key,
|
||||
'address': wallet_address
|
||||
}
|
||||
w = Wallet(port=8000, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS)
|
||||
res = w._backend.raw_request('check_tx_key', check_data)
|
||||
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:
|
||||
raise Exception('there was a problem i dont feel like writing good code for right now')
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
class EnumArrayField(pwpg.ArrayField):
|
||||
def __init__(self, enum_class, *args, **kwargs):
|
||||
"""
|
||||
Usage:
|
||||
from enum import IntEnum, unique
|
||||
|
||||
@unique
|
||||
class UserRoles(IntEnum):
|
||||
admin = 0
|
||||
moderator = 1
|
||||
guest = 2
|
||||
|
||||
[...]
|
||||
roles = EnumArrayField(enum_class=UserRoles, field_class=IntegerField, default=[UserRoles.guest])
|
||||
|
||||
# Fetch results with `admin` OR `moderator` role set:
|
||||
Model.select().where(
|
||||
Model.roles.contains_any(UserRoles.admin, UserRoles.moderator)).get()
|
||||
"""
|
||||
super(EnumArrayField, self).__init__(*args, **kwargs)
|
||||
self.enum_class = enum_class
|
||||
|
||||
def db_value(self, value):
|
||||
"""python -> database"""
|
||||
if not isinstance(value, (tuple, list)):
|
||||
raise TypeError("Wrong type, must be a list of enums")
|
||||
# if isinstance(value, tuple):
|
||||
# value = value[0]
|
||||
|
||||
data = []
|
||||
for enum in value:
|
||||
if not isinstance(enum, self.enum_class):
|
||||
raise TypeError("Wrong type, must be a list of enums")
|
||||
data.append(enum.value)
|
||||
return super().adapt(data)
|
||||
|
||||
def python_value(self, value):
|
||||
"""database -> python"""
|
||||
data = []
|
||||
for val in value:
|
||||
data.append(self.enum_class(val))
|
||||
return data
|
||||
|
||||
|
||||
class EnumIntField(pw.IntegerField):
|
||||
def __init__(self, enum_class, *args, **kwargs):
|
||||
"""
|
||||
Usage:
|
||||
from enum import IntEnum, unique
|
||||
|
||||
@unique
|
||||
class UserStatus(IntEnum):
|
||||
disabled = 0
|
||||
enable = 1
|
||||
banned = 2
|
||||
|
||||
[...]
|
||||
status = EnumIntField(enum_class=UserStatus, default=UserStatus.active)
|
||||
[...]
|
||||
Model.select().where(Model.status != UserStatus.banned)
|
||||
"""
|
||||
super(pw.IntegerField, self).__init__(*args, **kwargs)
|
||||
self.enum_class = enum_class
|
||||
|
||||
def db_value(self, value):
|
||||
return value.value
|
||||
|
||||
def python_value(self, value):
|
||||
return self.enum_class(value)
|
||||
|
@ -1,100 +1,230 @@
|
||||
from quart import Blueprint, render_template, flash, redirect, url_for
|
||||
from quart import Blueprint, render_template, flash, redirect, url_for, request
|
||||
from flask_login import current_user, login_required
|
||||
from monero.wallet import Wallet
|
||||
from monero.address import address
|
||||
from monero.numbers import to_atomic, from_atomic
|
||||
from monero.backends.jsonrpc.exceptions import RPCError
|
||||
|
||||
from xmrbackers.forms import ConfirmSubscription
|
||||
from xmrbackers.models import User, CreatorProfile, BackerProfile, TextPost
|
||||
from xmrbackers.models import Subscription, SubscriptionMeta
|
||||
from xmrbackers.helpers import check_tx_key
|
||||
from xmrbackers import config
|
||||
from xmrbackers.models import *
|
||||
from xmrbackers.helpers import make_wallet_rpc
|
||||
from xmrbackers import config, forms
|
||||
|
||||
|
||||
bp = Blueprint('creator', 'creator')
|
||||
|
||||
@bp.route('/creators')
|
||||
async def all():
|
||||
creators = CreatorProfile.select().order_by(
|
||||
CreatorProfile.create_date.desc()
|
||||
creators = User.select().join(Profile).where(
|
||||
User.roles.contains_any(UserRole.creator)
|
||||
).order_by(
|
||||
Profile.create_date.desc()
|
||||
)
|
||||
return await render_template('creator/creators.html', creators=creators)
|
||||
|
||||
@bp.route('/creator/<username>')
|
||||
async def show(username):
|
||||
user = User.select().where(User.username == username)
|
||||
creator = CreatorProfile.select().where(
|
||||
CreatorProfile.user == user
|
||||
).first()
|
||||
if creator:
|
||||
posts = TextPost.select().where(
|
||||
TextPost.creator == creator,
|
||||
TextPost.hidden == False
|
||||
).order_by(TextPost.post_date.desc())
|
||||
return await render_template(
|
||||
'creator/creator.html',
|
||||
creator=creator,
|
||||
posts=posts
|
||||
)
|
||||
else:
|
||||
await flash('That creator does not exist.')
|
||||
return redirect(url_for('meta.index'))
|
||||
|
||||
@bp.route('/creator/<username>/subscription')
|
||||
async def subscription(username):
|
||||
user = User.select().where(User.username == username)
|
||||
creator = CreatorProfile.select().where(
|
||||
CreatorProfile.user == user
|
||||
@bp.route('/creators/join', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
async def join():
|
||||
form = forms.ConfirmPlatformSubscription()
|
||||
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:
|
||||
await flash('You already are a creator!', 'warning')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
data = {
|
||||
'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',
|
||||
form=form
|
||||
)
|
||||
if creator:
|
||||
subscription_meta = SubscriptionMeta.select().where(
|
||||
SubscriptionMeta.creator == creator
|
||||
).order_by(SubscriptionMeta.create_date.desc()).first()
|
||||
form = ConfirmSubscription()
|
||||
return await render_template(
|
||||
'creator/subscription.html',
|
||||
subscription_meta=subscription_meta,
|
||||
form=form
|
||||
)
|
||||
else:
|
||||
await flash('That creator does not exist.')
|
||||
return redirect(url_for('meta.index'))
|
||||
|
||||
@bp.route('/subscription/<int:subscription_id>/confirm', methods=['POST'])
|
||||
async def confirm_subscription(subscription_id):
|
||||
# do checks here for SubscriptionMeta assumption
|
||||
sm = SubscriptionMeta.get_or_none(subscription_id)
|
||||
form = ConfirmSubscription()
|
||||
|
||||
|
||||
@bp.route('/creator/<handle>', methods=['GET', 'POST'])
|
||||
async def show(handle):
|
||||
form = forms.ConfirmCreatorSubscription()
|
||||
creator = User.select().join(Profile, pw.JOIN.LEFT_OUTER).where(
|
||||
User.handle == handle,
|
||||
User.roles.contains_any(UserRole.creator)
|
||||
).first()
|
||||
|
||||
if not creator:
|
||||
await flash('That creator does not exist.', 'warning')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
creator.derive_subscription_fees()
|
||||
|
||||
posts = Post.select().where(
|
||||
Post.creator == creator,
|
||||
Post.hidden == False
|
||||
).order_by(Post.post_date.desc())
|
||||
subscriptions = SubscriptionMeta.select().where(
|
||||
SubscriptionMeta.user == creator
|
||||
).order_by(SubscriptionMeta.create_date.desc())
|
||||
|
||||
if form.validate_on_submit():
|
||||
w = Wallet(
|
||||
port=8000,
|
||||
user=config.XMR_WALLET_RPC_USER,
|
||||
password=config.XMR_WALLET_RPC_PASS
|
||||
)
|
||||
check_data = {
|
||||
data = {
|
||||
'txid': form.tx_id.data,
|
||||
'tx_key': form.tx_key.data,
|
||||
'address': form.wallet_address.data
|
||||
}
|
||||
try:
|
||||
res = w._backend.raw_request('check_tx_key', check_data)
|
||||
except:
|
||||
await flash(f'Invalid transaction! No subscription for you!')
|
||||
return redirect(url_for('creator.show', username=sm.creator.user.username))
|
||||
|
||||
if res['received'] >= sm.atomic_xmr:
|
||||
backer_profile = BackerProfile.select().where(
|
||||
BackerProfile.user == current_user
|
||||
).first()
|
||||
s = Subscription(
|
||||
creator=sm.creator.id,
|
||||
backer=backer_profile.id,
|
||||
meta=sm.id,
|
||||
res = make_wallet_rpc('check_tx_key', data)
|
||||
subs = subscriptions.where(
|
||||
SubscriptionMeta.wallet_address == data['address']
|
||||
)
|
||||
s.save()
|
||||
await flash(f'Found valid transaction! You are now subscribed to {sm.creator.user.username}!')
|
||||
return redirect(url_for('creator.show', username=sm.creator.user.username))
|
||||
else:
|
||||
await flash('Not enough XMR sent! No subscription for you!')
|
||||
return redirect(url_for('creator.show', username=sm.creator.user.username))
|
||||
else:
|
||||
await flash('Unable to accept form POST.')
|
||||
return redirect(url_for('meta.index'))
|
||||
for sub in subs:
|
||||
if res['in_pool'] is False and res['received'] >= sub.atomic_xmr:
|
||||
await flash('Found a valid subscription.', 'success')
|
||||
print(request)
|
||||
return redirect(url_for('creator.show', handle=handle))
|
||||
except RPCError:
|
||||
await flash('Failed to check TX Key. Problem with provided info.', 'error')
|
||||
|
||||
return await render_template(
|
||||
'creator/show.html',
|
||||
creator=creator,
|
||||
subscription=subscriptions.first(),
|
||||
posts=posts,
|
||||
form=form
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/creator/subscriptions/manage', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
async def manage_subscriptions():
|
||||
form = forms.CreateSubscription()
|
||||
if UserRole.creator not in current_user.roles:
|
||||
await flash('You are not a creator!', 'warning')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
platform_subs = CreatorSubscription.select().where(
|
||||
CreatorSubscription.user == current_user
|
||||
).order_by(CreatorSubscription.create_date.desc())
|
||||
content_subs = SubscriptionMeta.select().where(
|
||||
SubscriptionMeta.user == current_user
|
||||
).order_by(SubscriptionMeta.create_date.desc())
|
||||
subscribers = Subscription.select().where(
|
||||
Subscription.meta.in_(content_subs)
|
||||
).order_by(Subscription.subscribe_date.desc())
|
||||
|
||||
if form.validate_on_submit():
|
||||
s = SubscriptionMeta(
|
||||
user=current_user,
|
||||
atomic_xmr=to_atomic(form.price_xmr.data),
|
||||
number_hours=form.number_days.data * 24.0,
|
||||
wallet_address=current_user.wallet_address
|
||||
)
|
||||
s.save()
|
||||
await flash('posting form data', 'success')
|
||||
|
||||
return await render_template(
|
||||
'creator/manage_subscriptions.html',
|
||||
platform_subs=platform_subs,
|
||||
content_subs=content_subs,
|
||||
subscribers=subscribers,
|
||||
form=form
|
||||
)
|
||||
|
||||
# @bp.route('/subscription/<int:subscription_id>/confirm', methods=['POST'])
|
||||
# async def confirm_subscription(subscription_id):
|
||||
# # do checks here for SubscriptionMeta assumption
|
||||
# sm = SubscriptionMeta.get_or_none(subscription_id)
|
||||
# form = ConfirmPlatformSubscription()
|
||||
# if form.validate_on_submit():
|
||||
# w = Wallet(
|
||||
# port=8000,
|
||||
# user=config.XMR_WALLET_RPC_USER,
|
||||
# password=config.XMR_WALLET_RPC_PASS
|
||||
# )
|
||||
# check_data = {
|
||||
# 'txid': form.tx_id.data,
|
||||
# 'tx_key': form.tx_key.data,
|
||||
# 'address': form.wallet_address.data
|
||||
# }
|
||||
# try:
|
||||
# res = w._backend.raw_request('check_tx_key', check_data)
|
||||
# except:
|
||||
# await flash(f'Invalid transaction! No subscription for you!')
|
||||
# return redirect(url_for('creator.show', username=sm.creator.user.username))
|
||||
|
||||
# if res['received'] >= sm.atomic_xmr:
|
||||
# backer_profile = BackerProfile.select().where(
|
||||
# BackerProfile.user == current_user
|
||||
# ).first()
|
||||
# s = Subscription(
|
||||
# creator=sm.creator.id,
|
||||
# backer=backer_profile.id,
|
||||
# meta=sm.id,
|
||||
# )
|
||||
# s.save()
|
||||
# await flash(f'Found valid transaction! You are now subscribed to {sm.creator.user.username}!')
|
||||
# return redirect(url_for('creator.show', username=sm.creator.user.username))
|
||||
# else:
|
||||
# await flash('Not enough XMR sent! No subscription for you!')
|
||||
# return redirect(url_for('creator.show', username=sm.creator.user.username))
|
||||
# else:
|
||||
# await flash('Unable to accept form POST.')
|
||||
# return redirect(url_for('meta.index'))
|
||||
|
@ -0,0 +1,30 @@
|
||||
from quart import Blueprint, render_template
|
||||
from flask_login import current_user
|
||||
|
||||
from xmrbackers.models import *
|
||||
|
||||
|
||||
bp = Blueprint('main', 'main')
|
||||
|
||||
@bp.route('/')
|
||||
async 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 await render_template(
|
||||
'index.html',
|
||||
feed=feed
|
||||
)
|
@ -1,30 +0,0 @@
|
||||
from quart import Blueprint, render_template
|
||||
from flask_login import current_user
|
||||
|
||||
from xmrbackers.models import CreatorProfile, Subscription, TextPost
|
||||
|
||||
|
||||
bp = Blueprint('meta', 'meta')
|
||||
|
||||
@bp.route('/')
|
||||
async def index():
|
||||
feed = None
|
||||
if current_user.is_authenticated:
|
||||
backer = current_user.backer_profile.first()
|
||||
new_creators = CreatorProfile.select().where(
|
||||
CreatorProfile.verified == True
|
||||
).order_by(CreatorProfile.create_date.desc()).execute()
|
||||
active_subscriptions = Subscription.select().where(
|
||||
Subscription.active == True,
|
||||
Subscription.backer == backer
|
||||
).order_by(Subscription.subscribe_date.desc()).execute()
|
||||
new_posts = TextPost.select().where(
|
||||
TextPost.hidden == False,
|
||||
TextPost.creator in [c.creator for c in active_subscriptions]
|
||||
).order_by(TextPost.post_date.desc()).execute()
|
||||
feed = {
|
||||
'new_creators': [i.user.username for i in new_creators],
|
||||
'new_posts': [i.title for i in new_posts],
|
||||
'active_subscriptions': [i.id for i in active_subscriptions]
|
||||
}
|
||||
return await render_template('index.html', feed=feed)
|
@ -1,27 +1,27 @@
|
||||
from quart import Blueprint, render_template, flash, redirect, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from xmrbackers.models import TextPost, Subscription
|
||||
from xmrbackers.models import Content, Subscription
|
||||
|
||||
|
||||
bp = Blueprint('post', 'post')
|
||||
|
||||
@bp.route('/post/<int:post_id>')
|
||||
async def show(post_id):
|
||||
post = TextPost.get_or_none(post_id)
|
||||
if post:
|
||||
if current_user.is_anonymous:
|
||||
await 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 await render_template('post/show.html', post=post)
|
||||
else:
|
||||
await 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'))
|
||||
# @bp.route('/post/<int:post_id>')
|
||||
# async def show(post_id):
|
||||
# post = TextPost.get_or_none(post_id)
|
||||
# if post:
|
||||
# if current_user.is_anonymous:
|
||||
# await 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 await render_template('post/show.html', post=post)
|
||||
# else:
|
||||
# await 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'))
|
||||
|
@ -0,0 +1,11 @@
|
||||
{% extends 'includes/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>Challenge</h2>
|
||||
<p>Handle: {{ user.handle }}</p>
|
||||
<p>Challenge: {{ user.challenge }}</p>
|
||||
<p>Wallet Address: {{ user.wallet_address }}</p>
|
||||
{% include 'includes/form.html' %}
|
||||
|
||||
{% endblock %}
|
@ -1,46 +1,8 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
{% extends 'includes/base.html' %}
|
||||
|
||||
{% include 'includes/head.html' %}
|
||||
{% block content %}
|
||||
|
||||
<body class="is-preload landing">
|
||||
<div id="page-wrapper">
|
||||
<h2>Login</h2>
|
||||
{% include 'includes/form.html' %}
|
||||
|
||||
{% include 'includes/header.html' %}
|
||||
|
||||
<section id="banner">
|
||||
<div class="content">
|
||||
<header>
|
||||
<h2>Login</h2>
|
||||
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||
|
||||
{% for f in form %}
|
||||
{% if f.name == 'csrf_token' %}
|
||||
{{ f }}
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
{{ f.label }}
|
||||
{{ f }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<ul>
|
||||
{% for field, errors in form.errors.items() %}
|
||||
<li>{{ form[field].label }}: {{ ', '.join(errors) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input type="submit" value="Login" class="btn btn-link btn-outline btn-xl">
|
||||
</form>
|
||||
</header>
|
||||
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'includes/scripts.html' %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
@ -1,46 +1,8 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
{% extends 'includes/base.html' %}
|
||||
|
||||
{% include 'includes/head.html' %}
|
||||
{% block content %}
|
||||
|
||||
<body class="is-preload landing">
|
||||
<div id="page-wrapper">
|
||||
<h2>Register</h2>
|
||||
{% include 'includes/form.html' %}
|
||||
|
||||
{% include 'includes/header.html' %}
|
||||
|
||||
<section id="banner">
|
||||
<div class="content">
|
||||
<header>
|
||||
<h2>Register</h2>
|
||||
<form method="POST" action="{{ url_for('auth.register') }}">
|
||||
|
||||
{% for f in form %}
|
||||
{% if f.name == 'csrf_token' %}
|
||||
{{ f }}
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
{{ f.label }}
|
||||
{{ f }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<ul>
|
||||
{% for field, errors in form.errors.items() %}
|
||||
<li>{{ form[field].label }}: {{ ', '.join(errors) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input type="submit" value="Register" class="btn btn-link btn-outline btn-xl">
|
||||
</form>
|
||||
</header>
|
||||
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'includes/scripts.html' %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
@ -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>
|
@ -0,0 +1,17 @@
|
||||
{% extends 'includes/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Become a Creator</h1>
|
||||
<p>
|
||||
Creator subscriptions have a flat rate of {{ config.CREATOR_SUBSCRIPTION_FEE_XMR }} XMR with a modifier for how much content is currently stored. <br>
|
||||
Subscriptions are a {{ config.CREATOR_SUBSCRIPTION_TERM }} day term. <br>
|
||||
Please send XMR to the platform wallet below and provide your <code>tx_hash</code> and <code>tx_key</code> in the form below to initiate your platform subscription.
|
||||
</p>
|
||||
<p>
|
||||
Subscription Price: {{ config.CREATOR_SUBSCRIPTION_FEE_XMR }} XMR <br>
|
||||
Platform Wallet: {{ config.PLATFORM_WALLET }}
|
||||
</p>
|
||||
|
||||
{% include 'includes/form.html' %}
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,49 @@
|
||||
{% extends 'includes/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>Platform Subscriptions</h2>
|
||||
<ul>
|
||||
{% for s in platform_subs %}
|
||||
<li>
|
||||
Platform Subscription ID: {{ s.id }} <br>
|
||||
Created: {{ s.create_date }} <br>
|
||||
TX ID: {{ s.tx.tx_id }} <br>
|
||||
Paid: {{ s.tx.atomic_xmr | from_atomic }} XMR <br>
|
||||
Term Length: {{ s.term_hours }} hours ({{ s.hours_until_content_hidden }} hours) <br>
|
||||
Hide Content: {{ s.hours_until_content_hidden }} hours <br>
|
||||
Archive Content: {{ s.hours_until_content_archived }} hours <br>
|
||||
Active Subscription: <strong>{{ s.is_active }}</strong> <br>
|
||||
<br>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Content Subscriptions</h2>
|
||||
{% if not content_subs %}
|
||||
<p>No subscriptions to your content yet.</p>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for s in content_subs %}
|
||||
<li>
|
||||
Content Subscription ID: {{ s.id }} <br>
|
||||
Created: {{ s.create_date }} <br>
|
||||
Price: {{ s.atomic_xmr | from_atomic }} XMR <br>
|
||||
Term Length: {{ s.number_hours }} hours <br>
|
||||
Active Subscriptions: {{ s.get_active_subscriptions().count() }} <br>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% include 'includes/form.html' %}
|
||||
|
||||
<h2>Subscribers</h2>
|
||||
{% if not subscribers %}
|
||||
<p>No subscribers yet.</p>
|
||||
{% else %}
|
||||
{% for subscriber in subscribers %}
|
||||
{{ subscriber }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,38 @@
|
||||
{% extends 'includes/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ creator.handle }}</h1>
|
||||
{% if creator.bio %}<p>Bio: {{ creator.bio }}</p>{% endif %}
|
||||
|
||||
<h2>Subscribe</h2>
|
||||
{% if subscription %}
|
||||
<p>
|
||||
Send <code>{{ subscription.atomic_xmr | from_atomic | round(2) }}</code> XMR to
|
||||
<code>{{ subscription.wallet_address }}</code>
|
||||
and provide your TX ID and TX KEY to subscribe to this user
|
||||
and view their content for the next {{ subscription.number_hours / 24 }} days
|
||||
({{ subscription.number_hours }} hours).
|
||||
</p>
|
||||
{% else %}
|
||||
<p>This creator has not setup a subscription plan yet.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if not current_user.is_subscribed(subscription) %}
|
||||
{% include 'includes/form.html' %}
|
||||
{% else %}
|
||||
subscribed
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p><a href="{{ url_for('auth.login') }}">Login to subscribe.</a></p>
|
||||
{% endif %}
|
||||
|
||||
<h2>Content</h2>
|
||||
{% if not posts %}<p>This creator has not posted any content yet.</p>{% endif %}
|
||||
{% 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 %}
|
||||
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
{% include 'includes/head.html' %}
|
||||
<body style="background-color: black; color: white;">
|
||||
{% include 'includes/header.html' %}
|
||||
{% block content %}{% endblock %}
|
||||
{% include 'includes/debug.html' %}
|
||||
{% include 'includes/scripts.html' %}
|
||||
<style>
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
a, a:visited {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,9 @@
|
||||
{% if config.DEBUG and current_user.is_authenticated %}
|
||||
<hr>
|
||||
<h2>Debug</h2>
|
||||
<p>
|
||||
Username: {{ current_user.handle }} <br>
|
||||
{% if current_user.email %}Email: {{ current_user.email }} <br>{% endif %}
|
||||
Wallet Address: {{ current_user.wallet_address }} <br>
|
||||
</p>
|
||||
{% endif %}
|
@ -0,0 +1,18 @@
|
||||
<form method="POST" action="">
|
||||
{% for f in form %}
|
||||
{% if f.name == 'csrf_token' %}
|
||||
{{ f }}
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
{{ f.label }}
|
||||
{{ f }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<ul>
|
||||
{% for field, errors in form.errors.items() %}
|
||||
<li>{{ form[field].label }}: {{ ', '.join(errors) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input type="submit" value="Confirm" class="btn btn-link btn-outline btn-xl">
|
||||
</form>
|
@ -1,40 +1,35 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
{% include 'includes/head.html' %}
|
||||
|
||||
<body class="is-preload landing">
|
||||
<div id="page-wrapper">
|
||||
|
||||
{% include 'includes/header.html' %}
|
||||
|
||||
{% if feed %}
|
||||
{% for k in feed %}
|
||||
<h2>{{ k }}</h2>
|
||||
{% if k == 'new_creators' %}
|
||||
<p><a href="{{ url_for('creator.show', username=feed[k]) }}">{{ feed[k] }}</a></p>
|
||||
{% else %}
|
||||
<p>{{ feed[k]}}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
||||
<section id="banner">
|
||||
<div class="content">
|
||||
<header>
|
||||
<p>This is a simple prototype and is under heavy development.</p>
|
||||
</header>
|
||||
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% include 'includes/footer.html' %}
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'includes/scripts.html' %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% extends 'includes/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if 2 not in current_user.roles %}
|
||||
<a href="{{ url_for('creator.join') }}">Become a Creator</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('creator.manage_subscriptions') }}">Manage Subscriptions</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if feed %}
|
||||
<h1>Feed</h1>
|
||||
{% if feed['new_creators'] %}
|
||||
<h2>New Creators</h2>
|
||||
{% for c in feed['new_creators'] %}
|
||||
<p><a href="{{ url_for('creator.show', handle=c.handle) }}">{{ c.handle }}</a></p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if feed['new_posts'] %}
|
||||
<h2>New Posts</h2>
|
||||
{% for p in feed['new_posts'] %}
|
||||
<p>{{ p.id }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if feed['active_subscriptions'] %}
|
||||
<h2>Active Subscriptions</h2>
|
||||
{% for s in feed['active_subscriptions'] %}
|
||||
<p>{{ s.id }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue