Compare commits

...

9 Commits
main ... revamp

@ -1,7 +1,7 @@
FROM ubuntu:21.10 FROM ubuntu:22.04
ENV MONERO_HASH 59e16c53b2aff8d9ab7a8ba3279ee826ac1f2480fbb98e79a149e6be23dd9086 ENV MONERO_HASH 937dfcc48d91748dd2e8f58714dfc45d17a0959dff33fc7385bbe06344ff2c16
ENV MONERO_DL_URL https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.2.0.tar.bz2 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_DL_FILE monero.tar.bz2
ENV MONERO_SUMS_FILE sha256sums ENV MONERO_SUMS_FILE sha256sums

@ -10,14 +10,20 @@ 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
dev: ## Start Quart development web server 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

@ -1,2 +1,6 @@
# lza-quart-app # xmrbackers
Template project for Quart (Python/Flask) applications.
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() app = create_app()
if __name__ == '__main__': 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)

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
export RPC_CREDS="${2}" export RPC_CREDS="${2}"
export DAEMON_ADDRESS=singapore.node.xmr.pm export DAEMON_ADDRESS="${3}"
# Define the network we plan to operate our wallet in # Define the network we plan to operate our wallet in
if [[ "${1}" == "stagenet" ]]; then if [[ "${1}" == "stagenet" ]]; then
@ -19,7 +19,7 @@ fi
if [[ ! -d /data/wallet ]]; then if [[ ! -d /data/wallet ]]; then
monero-wallet-cli ${NETWORK} \ monero-wallet-cli ${NETWORK} \
--generate-new-wallet /data/wallet \ --generate-new-wallet /data/wallet \
--daemon-address http://${DAEMON_ADDRESS}:${PORT} \ --daemon-address ${DAEMON_ADDRESS} \
--trusted-daemon \ --trusted-daemon \
--use-english-language-names \ --use-english-language-names \
--mnemonic-language English --mnemonic-language English
@ -27,7 +27,7 @@ fi
# Run RPC wallet # Run RPC wallet
monero-wallet-rpc ${NETWORK} \ monero-wallet-rpc ${NETWORK} \
--daemon-address http://${DAEMON_ADDRESS}:${PORT} \ --daemon-address ${DAEMON_ADDRESS} \
--wallet-file /data/wallet \ --wallet-file /data/wallet \
--password "" \ --password "" \
--rpc-login ${RPC_CREDS} \ --rpc-login ${RPC_CREDS} \

@ -19,8 +19,8 @@ services:
context: . context: .
dockerfile: Dockerfile-monero dockerfile: Dockerfile-monero
ports: ports:
- 127.0.0.1:8000:8000 - 127.0.0.1:${XMR_WALLET_RPC_PORT:-8000}:${XMR_WALLET_RPC_PORT:-8000}
volumes: volumes:
- ./data/wallet:/data - ${DATA_DIR:-./data/wallet}:/data
command: command:
bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}" bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}" "${XMR_DAEMON_URI}"

@ -3,11 +3,11 @@ DB_USER=xmrbackers
DB_NAME=xmrbackers DB_NAME=xmrbackers
DB_HOST=localhost DB_HOST=localhost
XMR_WALLET_PATH=/data/xmr-wallet PLATFORM_WALLET=xxxxxx
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_ENDPOINT=http://localhost:9090 XMR_WALLET_RPC_PORT=8000
XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089 XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089
XMR_WALLET_NETWORK=stagenet XMR_WALLET_NETWORK=stagenet

@ -9,9 +9,8 @@ qrcode
redis redis
peewee peewee
requests requests
SQLAlchemy
WTForms WTForms
quart quart==0.17.0
monero monero
arrow arrow
flake8 flake8

@ -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,8 +1,9 @@
from dotenv import load_dotenv
from secrets import token_urlsafe from secrets import token_urlsafe
from datetime import timedelta from datetime import timedelta
from os import getenv from os import getenv
from dotenv import load_dotenv
load_dotenv() load_dotenv()
@ -11,13 +12,21 @@ SITE_NAME = getenv('SITE_NAME', 'xmrbackers')
SECRET_KEY = getenv('SECRET_KEY') SECRET_KEY = getenv('SECRET_KEY')
STATS_TOKEN = getenv('STATS_TOKEN', token_urlsafe(8)) STATS_TOKEN = getenv('STATS_TOKEN', token_urlsafe(8))
SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000') SERVER_NAME = getenv('SERVER_NAME', 'localhost:5000')
PLATFORM_WALLET = getenv('PLATFORM_WALLET')
# Constants - here for easy template references
CREATOR_SUBSCRIPTION_TERM = 30 # days term of how long creator subscriptions are valid for until content is hidden
CREATOR_SUBSCRIPTION_GRACE = 21 # days grace period after expiration of creator subscriptions until content is archived
CREATOR_SUBSCRIPTION_FEE_XMR = .15 # XMR flat rate fee for creator subscriptions
CREATOR_CONTENT_FEE_PERCENT = 5 # percentage of base fee to charge for content/posts
CREATOR_ACTIVE_SUBSCRIBER_FEE_PERCENT = 10 # percentage of received subscriber fees to charge creator for platform reup
# Crypto RPC # Crypto RPC
XMR_WALLET_PASS = getenv('XMR_WALLET_PASS') XMR_WALLET_PASS = getenv('XMR_WALLET_PASS')
XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER') XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER')
XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS') XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS')
XMR_WALLET_RPC_ENDPOINT = getenv('XMR_WALLET_RPC_ENDPOINT') 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 # Database
DB_HOST = getenv('DB_HOST', 'localhost') DB_HOST = getenv('DB_HOST', 'localhost')
@ -31,7 +40,7 @@ REDIS_HOST = getenv('REDIS_HOST', 'localhost')
REDIS_PORT = getenv('REDIS_PORT', 6379) REDIS_PORT = getenv('REDIS_PORT', 6379)
# Sessions # Sessions
SESSION_LENGTH = int(getenv('SESSION_LENGTH', 30)) SESSION_LENGTH = int(getenv('SESSION_LENGTH', 300))
PERMANENT_SESSION_LIFETIME = timedelta(minutes=SESSION_LENGTH) PERMANENT_SESSION_LIFETIME = timedelta(minutes=SESSION_LENGTH)
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 MAX_CONTENT_LENGTH = 50 * 1024 * 1024

@ -1,10 +1,8 @@
import quart.flask_patch import quart.flask_patch
from quart import Quart from quart import Quart
from flask_bcrypt import Bcrypt
from flask_login import LoginManager from flask_login import LoginManager
from xmrbackers.cli import cli from xmrbackers.cli import cli
from xmrbackers import config
async def _setup_db(app: Quart): async def _setup_db(app: Quart):
@ -20,10 +18,10 @@ def create_app():
app.config.from_envvar('QUART_SECRETS') app.config.from_envvar('QUART_SECRETS')
@app.before_serving @app.before_serving
async def startup(): async def startup():
from xmrbackers.routes import meta, api, auth, creator, post from xmrbackers.routes import api, auth, creator, post, main
from xmrbackers import filters from xmrbackers import filters
await _setup_db(app) await _setup_db(app)
app.register_blueprint(meta.bp) app.register_blueprint(main.bp)
app.register_blueprint(api.bp) app.register_blueprint(api.bp)
app.register_blueprint(auth.bp) app.register_blueprint(auth.bp)
app.register_blueprint(creator.bp) app.register_blueprint(creator.bp)
@ -41,5 +39,3 @@ def create_app():
user = User.get_or_none(user_id) user = User.get_or_none(user_id)
return user return user
return app return app
bcrypt = Bcrypt(create_app())

@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
import arrow import arrow
import monero from monero import numbers
from quart import Blueprint, current_app from quart import Blueprint, current_app
@ -22,4 +22,4 @@ def xmr_block_explorer(v):
@bp.app_template_filter('from_atomic') @bp.app_template_filter('from_atomic')
def from_atomic(amt): def from_atomic(amt):
return monero.numbers.from_atomic(amt) return numbers.from_atomic(amt)

@ -1,14 +1,46 @@
# import quart.flask_patch
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField from wtforms import StringField, FloatField
from wtforms.validators import DataRequired 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): def is_valid_xmr_address(form, field):
tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={"placeholder": "TX ID", "class": "form-control", "type": "text"}) try:
tx_key = StringField('TX Key:', validators=[DataRequired()], render_kw={"placeholder": "TX Key", "class": "form-control", "type": "text"}) # Ensure the provided address is valid address/subaddress/integrated address
wallet_address = StringField('XMR Address:', validators=[DataRequired()], render_kw={"placeholder": "XMR Address", "class": "form-control", "type": "text"}) 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.wallet import Wallet
from monero.exceptions import WrongAddress
from xmrbackers import config from xmrbackers import config
def check_tx_key(tx_id, tx_key, wallet_address):
def make_wallet_rpc(method, data={}):
try: try:
check_data = { w = Wallet(
'txid': tx_id, port=config.XMR_WALLET_RPC_PORT,
'tx_key': tx_key, user=config.XMR_WALLET_RPC_USER,
'address': wallet_address password=config.XMR_WALLET_RPC_PASS,
} timeout=3
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) res = w._backend.raw_request(method, data)
return res return res
except: except Exception as e:
raise Exception('there was a problem i dont feel like writing good code for right now') 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,8 +1,13 @@
from datetime import datetime from datetime import datetime, timedelta
from enum import IntEnum, unique
from typing import List
from secrets import token_urlsafe
import peewee as pw import peewee as pw
from monero.numbers import to_atomic, from_atomic
from xmrbackers import config from xmrbackers import config
from xmrbackers.helpers import EnumArrayField, EnumIntField
db = pw.PostgresqlDatabase( db = pw.PostgresqlDatabase(
@ -12,16 +17,44 @@ db = pw.PostgresqlDatabase(
host=config.DB_HOST, host=config.DB_HOST,
) )
def gen_challenge():
return token_urlsafe().replace('-', '').replace('_', '')
@unique
class UserRole(IntEnum):
admin = 0
backer = 1
creator = 2
@unique
class PostType(IntEnum):
text = 0
gallery = 1
stream = 2
@unique
class ContentType(IntEnum):
text = 0
image = 1
video = 2
stream = 3
class User(pw.Model): class User(pw.Model):
""" """
User model is for pure user authentication management User model is for base user management and reporting.
and reporting.
""" """
id = pw.AutoField() id = pw.AutoField()
register_date = pw.DateTimeField(default=datetime.now) register_date = pw.DateTimeField(default=datetime.utcnow)
last_login_date = pw.DateTimeField(default=datetime.now) last_login_date = pw.DateTimeField(default=datetime.utcnow)
username = pw.CharField(unique=True) handle = pw.CharField(unique=True)
password = pw.CharField() wallet_address = pw.CharField(unique=True)
email = pw.CharField(unique=True, null=True)
challenge = pw.CharField(default=gen_challenge)
roles: List[UserRole] = EnumArrayField(enum_class=UserRole, field_class=pw.IntegerField, default=[UserRole.backer])
@property @property
def is_authenticated(self): def is_authenticated(self):
@ -37,46 +70,117 @@ class User(pw.Model):
@property @property
def is_admin(self): def is_admin(self):
return self.admin return UserRole.admin in self.roles
def get_id(self): def get_id(self):
return self.id return self.id
def regenerate_challenge(self):
self.challenge = gen_challenge()
self.save()
def is_subscribed(self, subscription):
s = Subscription.select().where(
Subscription.backer == self
).first()
if s:
return True
return False
def derive_subscription_fees(self):
base_fee_atomic = to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR)
posts = Post.select().where(Post.creator == self)
content = Content.select().join(Post).where(Post.creator == self)
content_base_fee_atomic = base_fee_atomic * config.CREATOR_CONTENT_FEE_PERCENT
content_fee_atomic = (posts.count() + content.count()) * content_base_fee_atomic
active_subs = Subscription.select().where(
Subscription.creator == self,
Subscription.is_active == True
)
received_sub_xmr_atomic = 0
for sub in active_subs:
received_sub_xmr_atomic += sub.meta.atomic_xmr
print('User can expect to pay the following fees on their next reup:\nBase Fee: {} XMR\nContent Fees: {} ({} posts, {} uploads)\nSubscriber Fees: {}'.format(
config.CREATOR_SUBSCRIPTION_FEE_XMR,
from_atomic(content_fee_atomic),
posts.count(),
content.count(),
from_atomic(received_sub_xmr_atomic)
))
class Meta: class Meta:
database = db database = db
class CreatorProfile(pw.Model): class Profile(pw.Model):
""" """
CreatorProfile model is for creators to provide metadata about Profile model is for users to provide metadata about
themselves for their fans or even just the general public. themselves; Creators for their fans or even just the general public.
Links to social media, contact info, portfolio sites, etc Links to social media, contact info, portfolio sites, etc
should go in here. should go in here.
""" """
id = pw.AutoField() id = pw.AutoField()
user = pw.ForeignKeyField(User) user = pw.ForeignKeyField(User)
create_date = pw.DateTimeField(default=datetime.now) create_date = pw.DateTimeField(default=datetime.utcnow)
wallet_address = pw.CharField(null=True)
website = pw.CharField(null=True) website = pw.CharField(null=True)
twitter_handle = pw.CharField(null=True) twitter_handle = pw.CharField(null=True)
email = pw.CharField(unique=True, null=True) bio = pw.CharField(null=True)
bio = pw.CharField()
verified = pw.CharField(default=False) verified = pw.CharField(default=False)
class Meta: class Meta:
database = db database = db
class BackerProfile(pw.Model): class Transaction(pw.Model):
""" """
BackerProfile model is for backers to give contact info Transaction model is a simple reference to a Monero transaction so that we can track
if they wanted to retain communications in some way...ie which transactions have occurred on-chain and which subscription they correspond to.
recurring emails and/or notifications. For now.
""" """
id = pw.AutoField() id = pw.AutoField()
user = pw.ForeignKeyField(User, backref='backer_profile') tx_id = pw.CharField(unique=True)
register_date = pw.DateTimeField(default=datetime.now) atomic_xmr = pw.BigIntegerField()
last_login_date = pw.DateTimeField(default=datetime.now) to_address = pw.CharField()
email = pw.CharField(unique=True, null=True)
class Meta:
database = db
class CreatorSubscription(pw.Model):
"""
CreatorSubscription model is for tracking subscriptions of creators to the platform.
Subscription is a flat fee with a modifier 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)
@property
def grace_start_date(self):
# Hide content after this date if no new valid platform subscription
return self.create_date + timedelta(hours=self.term_hours)
@property
def delete_start_date(self):
# Archive content after this date if no new valid platform subscription
return self.grace_start_date + timedelta(hours=self.grace_hours)
@property
def hours_until_content_hidden(self):
return self.grace_start_date - datetime.utcnow()
@property
def hours_until_content_archived(self):
return self.delete_start_date - datetime.utcnow()
@property
def is_active(self):
return self.grace_start_date > datetime.utcnow()
class Meta: class Meta:
database = db database = db
@ -85,20 +189,21 @@ class BackerProfile(pw.Model):
class SubscriptionMeta(pw.Model): class SubscriptionMeta(pw.Model):
""" """
SubscriptionMeta model is for the Creator to define details about 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 editing in place, only creating new plans; anyone utilizing an
existing subscription (by loading it on screen) will be grandfathered in. existing subscription (by loading it on screen) will be grandfathered in.
""" """
id = pw.AutoField() id = pw.AutoField()
create_date = pw.DateTimeField(default=datetime.now) user = pw.ForeignKeyField(User)
creator = pw.ForeignKeyField(CreatorProfile) create_date = pw.DateTimeField(default=datetime.utcnow)
atomic_xmr = pw.BigIntegerField() atomic_xmr = pw.BigIntegerField()
number_hours = pw.IntegerField() number_hours = pw.IntegerField()
wallet_address = pw.CharField() wallet_address = pw.CharField()
def get_end_date(self) -> datetime: def get_active_subscriptions(self):
# some timedelta shiz return Subscription.select().where(
pass Subscription.meta == self
)
class Meta: class Meta:
database = db database = db
@ -106,33 +211,56 @@ class SubscriptionMeta(pw.Model):
class Subscription(pw.Model): class Subscription(pw.Model):
""" """
Subscription model gets created when backers can confirm payment via Subscription model gets created when users/backers can confirm payment via
the `check_tx_key` RPC method. Once a subscription is in place and is the `check_tx_key` RPC method to the creator's wallet. Once a subscription
associated with a user, that user is then elligible to view that is in place and is associated with a user, that user is then elligible to
creator's content. view that creator's content until the subscription expires (number_hours).
""" """
id = pw.AutoField() id = pw.AutoField()
subscribe_date = pw.DateTimeField(default=datetime.now) subscribe_date = pw.DateTimeField(default=datetime.utcnow)
active = pw.BooleanField(default=True) tx = pw.ForeignKeyField(Transaction)
creator = pw.ForeignKeyField(CreatorProfile) creator = pw.ForeignKeyField(User)
backer = pw.ForeignKeyField(BackerProfile) backer = pw.ForeignKeyField(User)
meta = pw.ForeignKeyField(SubscriptionMeta) meta = pw.ForeignKeyField(SubscriptionMeta)
@property
def is_active(self):
end_date = self.subscribe_date + timedelta(hours=self.meta.number_hours)
return end_date > datetime.utcnow()
class Meta: class Meta:
database = db database = db
class TextPost(pw.Model):
class Post(pw.Model):
""" """
TextPost model is the first content type available to post. Metadata Post model represents a post from a creator consisting of Content objects
here is basic for now, let's proof out the other components first. which is only viewable by backers with an active subscription.
""" """
id = pw.AutoField() id = pw.AutoField()
post_date = pw.DateTimeField(default=datetime.now) post_date = pw.DateTimeField(default=datetime.utcnow)
hidden = pw.BooleanField(default=False) hidden = pw.BooleanField(default=False)
content = pw.TextField() content = pw.TextField()
title = pw.CharField() title = pw.CharField()
last_edit_date = pw.DateTimeField(default=datetime.now) last_edit_date = pw.DateTimeField(default=datetime.utcnow)
creator = pw.ForeignKeyField(CreatorProfile) creator = pw.ForeignKeyField(User)
type: List[PostType] = EnumIntField(enum_class=PostType, default=PostType.text)
class Meta:
database = db
class Content(pw.Model):
"""
Content model is any uploaded content from a creator.
"""
id = pw.AutoField()
location = pw.CharField()
upload_date = pw.DateTimeField(default=datetime.utcnow)
post = pw.ForeignKeyField(Post)
type: List[ContentType] = EnumIntField(enum_class=ContentType, default=ContentType.text)
class Meta: class Meta:
database = db database = db

@ -2,8 +2,8 @@ from quart import Blueprint, render_template
from quart import flash, redirect, url_for from quart import flash, redirect, url_for
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
from xmrbackers.factory import bcrypt from xmrbackers.forms import UserLogin, UserRegistration, UserChallenge
from xmrbackers.forms import UserAuth from xmrbackers.helpers import make_wallet_rpc
from xmrbackers.models import User from xmrbackers.models import User
@ -11,60 +11,87 @@ bp = Blueprint('auth', 'auth')
@bp.route("/register", methods=["GET", "POST"]) @bp.route("/register", methods=["GET", "POST"])
async def register(): async def register():
form = UserAuth() form = UserRegistration()
if current_user.is_authenticated: if current_user.is_authenticated:
await flash('Already registered and authenticated.') await flash('Already registered and authenticated.')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
if form.validate_on_submit(): if form.validate_on_submit():
# Check if username already exists # Check if handle already exists
user = User.select().where( user = User.select().where(
User.username == form.username.data User.handle == form.handle.data
).first() ).first()
if user: if user:
await flash('This username is already registered.') await flash('This handle is already registered.')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
# Save new user # Save new user
user = User( user = User(
username=form.username.data, handle=form.handle.data,
password=bcrypt.generate_password_hash(form.password.data).decode('utf8'), wallet_address=form.wallet_address.data,
) )
user.save() user.save()
login_user(user) login_user(user)
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
return await render_template("auth/register.html", form=form) return await render_template("auth/register.html", form=form)
@bp.route("/login", methods=["GET", "POST"]) @bp.route("/login", methods=["GET", "POST"])
async def login(): async def login():
form = UserAuth() form = UserLogin()
if current_user.is_authenticated: if current_user.is_authenticated:
await flash('Already logged in.') await flash('Already logged in.')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
if form.validate_on_submit(): if form.validate_on_submit():
# Check if user doesn't exist # Check if user doesn't exist
user = User.select().where( user = User.select().where(
User.username == form.username.data User.handle == form.handle.data
).first() ).first()
if not user: if not user:
await flash('Invalid username or password.') await flash('That handle does not exist.')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
return redirect(url_for('auth.challenge', handle=user.handle))
# Check if password is correct return await render_template("auth/login.html", form=form)
password_matches = bcrypt.check_password_hash(
user.password,
form.password.data
)
if not password_matches:
await flash('Invalid username or password.')
return redirect(url_for('auth.login'))
login_user(user) @bp.route("/login/challenge/<handle>", methods=["GET", "POST"])
return redirect(url_for('meta.index')) async def challenge(handle):
form = UserChallenge()
user = User.select().where(User.handle == handle).first()
if not user:
await flash('User does not exist.')
return redirect(url_for('main.index'))
return await render_template("auth/login.html", form=form) if current_user.is_authenticated:
await 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)
await flash('Successful login!')
return redirect(url_for('main.index'))
else:
await flash('Invalid signature. Try again.')
return redirect(url_for('auth.challenge', handle=handle))
except Exception as e:
await flash(f'Issue with checking the signature provided: {e}')
return redirect(url_for('auth.challenge', handle=handle))
return await render_template(
'auth/challenge.html',
user=user,
form=form
)
@bp.route("/logout") @bp.route("/logout")
async def logout(): async def logout():
@ -72,30 +99,4 @@ async def logout():
logout_user() logout_user()
else: else:
await flash('Not authenticated!') await flash('Not authenticated!')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
# @auth_bp.route("/reset/<string:hash>", methods=["GET", "POST"])
# def reset(hash):
# hash = PasswordReset.query.filter(PasswordReset.hash==hash).first()
# if not hash:
# flash('Invalid password reset hash')
# return redirect(url_for('auth.login'))
#
# if hash.hours_elapsed() > hash.expiration_hours or hash.expired:
# flash('Reset hash has expired')
# return redirect(url_for('auth.login'))
#
# form = ResetPassword()
# if form.validate_on_submit():
# try:
# user = User.query.get(hash.user)
# user.password = bcrypt.generate_password_hash(form.password.data).decode('utf8')
# hash.expired = True
# db.session.commit()
# flash('Password reset successfully')
# return redirect(url_for('auth.login'))
# except:
# flash('Error resetting password')
# return redirect(url_for('auth.login'))
#
# return render_template('auth/reset.html', form=form)

@ -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 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 monero.backends.jsonrpc.exceptions import RPCError
from xmrbackers.forms import ConfirmSubscription from xmrbackers.models import *
from xmrbackers.models import User, CreatorProfile, BackerProfile, TextPost from xmrbackers.helpers import make_wallet_rpc
from xmrbackers.models import Subscription, SubscriptionMeta from xmrbackers import config, forms
from xmrbackers.helpers import check_tx_key
from xmrbackers import config
bp = Blueprint('creator', 'creator') bp = Blueprint('creator', 'creator')
@bp.route('/creators') @bp.route('/creators')
async def all(): async def all():
creators = CreatorProfile.select().order_by( creators = User.select().join(Profile).where(
CreatorProfile.create_date.desc() User.roles.contains_any(UserRole.creator)
).order_by(
Profile.create_date.desc()
) )
return await render_template('creator/creators.html', creators=creators) return await render_template('creator/creators.html', creators=creators)
@bp.route('/creator/<username>') @bp.route('/creators/join', methods=['GET', 'POST'])
async def show(username): @login_required
user = User.select().where(User.username == username) async def join():
creator = CreatorProfile.select().where( form = forms.ConfirmPlatformSubscription()
CreatorProfile.user == user valid_address = False
).first()
if creator: try:
posts = TextPost.select().where( address(config.PLATFORM_WALLET)
TextPost.creator == creator, valid_address = True
TextPost.hidden == False except:
).order_by(TextPost.post_date.desc()) pass
return await render_template(
'creator/creator.html', if not config.PLATFORM_WALLET or valid_address is False:
creator=creator, await flash('Platform operator has not setup wallet yet. Try later.', 'warning')
posts=posts return redirect(url_for('main.index'))
)
else: if UserRole.creator in current_user.roles:
await flash('That creator does not exist.') await flash('You already are a creator!', 'warning')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
@bp.route('/creator/<username>/subscription') if form.validate_on_submit():
async def subscription(username): try:
user = User.select().where(User.username == username) data = {
creator = CreatorProfile.select().where( 'txid': form.tx_id.data,
CreatorProfile.user == user '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 @bp.route('/creator/<handle>', methods=['GET', 'POST'])
).order_by(SubscriptionMeta.create_date.desc()).first() async def show(handle):
form = ConfirmSubscription() form = forms.ConfirmCreatorSubscription()
return await render_template( creator = User.select().join(Profile, pw.JOIN.LEFT_OUTER).where(
'creator/subscription.html', User.handle == handle,
subscription_meta=subscription_meta, User.roles.contains_any(UserRole.creator)
form=form ).first()
)
else: if not creator:
await flash('That creator does not exist.') await flash('That creator does not exist.', 'warning')
return redirect(url_for('meta.index')) return redirect(url_for('main.index'))
@bp.route('/subscription/<int:subscription_id>/confirm', methods=['POST']) creator.derive_subscription_fees()
async def confirm_subscription(subscription_id):
# do checks here for SubscriptionMeta assumption posts = Post.select().where(
sm = SubscriptionMeta.get_or_none(subscription_id) Post.creator == creator,
form = ConfirmSubscription() 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(): if form.validate_on_submit():
w = Wallet( data = {
port=8000,
user=config.XMR_WALLET_RPC_USER,
password=config.XMR_WALLET_RPC_PASS
)
check_data = {
'txid': form.tx_id.data, 'txid': form.tx_id.data,
'tx_key': form.tx_key.data, 'tx_key': form.tx_key.data,
'address': form.wallet_address.data 'address': form.wallet_address.data
} }
try: try:
res = w._backend.raw_request('check_tx_key', check_data) res = make_wallet_rpc('check_tx_key', data)
except: subs = subscriptions.where(
await flash(f'Invalid transaction! No subscription for you!') SubscriptionMeta.wallet_address == data['address']
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() for sub in subs:
await flash(f'Found valid transaction! You are now subscribed to {sm.creator.user.username}!') if res['in_pool'] is False and res['received'] >= sub.atomic_xmr:
return redirect(url_for('creator.show', username=sm.creator.user.username)) await flash('Found a valid subscription.', 'success')
else: print(request)
await flash('Not enough XMR sent! No subscription for you!') return redirect(url_for('creator.show', handle=handle))
return redirect(url_for('creator.show', username=sm.creator.user.username)) except RPCError:
else: await flash('Failed to check TX Key. Problem with provided info.', 'error')
await flash('Unable to accept form POST.')
return redirect(url_for('meta.index')) 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 quart import Blueprint, render_template, flash, redirect, url_for
from flask_login import current_user from flask_login import current_user
from xmrbackers.models import TextPost, Subscription from xmrbackers.models import Content, Subscription
bp = Blueprint('post', 'post') bp = Blueprint('post', 'post')
@bp.route('/post/<int:post_id>') # @bp.route('/post/<int:post_id>')
async def show(post_id): # async def show(post_id):
post = TextPost.get_or_none(post_id) # post = TextPost.get_or_none(post_id)
if post: # if post:
if current_user.is_anonymous: # if current_user.is_anonymous:
await flash('You must login to view this post.') # await flash('You must login to view this post.')
return redirect(url_for('creator.show', username=post.creator.user.username)) # return redirect(url_for('creator.show', username=post.creator.user.username))
user_subscriptions = Subscription.select().where( # user_subscriptions = Subscription.select().where(
Subscription.active == True, # Subscription.active == True,
Subscription.backer == current_user.backer_profile.first() # Subscription.backer == current_user.backer_profile.first()
) # )
if user_subscriptions: # if user_subscriptions:
return await render_template('post/show.html', post=post) # return await render_template('post/show.html', post=post)
else: # else:
await flash('Viewing posts requires a subscription.') # await flash('Viewing posts requires a subscription.')
return redirect(url_for('creator.subscription', username=post.creator.user.username)) # return redirect(url_for('creator.subscription', username=post.creator.user.username))
else: # else:
flash('That post does not exist.') # flash('That post does not exist.')
return redirect(url_for('meta.index')) # 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> {% extends 'includes/base.html' %}
<html>
{% include 'includes/head.html' %} {% block content %}
<body class="is-preload landing"> <h2>Login</h2>
<div id="page-wrapper"> {% include 'includes/form.html' %}
{% include 'includes/header.html' %} {% endblock %}
<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>

@ -1,46 +1,8 @@
<!DOCTYPE HTML> {% extends 'includes/base.html' %}
<html>
{% include 'includes/head.html' %} {% block content %}
<body class="is-preload landing"> <h2>Register</h2>
<div id="page-wrapper"> {% include 'includes/form.html' %}
{% include 'includes/header.html' %} {% endblock %}
<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>

@ -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 %}

@ -1,3 +1,14 @@
<hr>
<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>
<footer id="footer"> <footer id="footer">
<ul class="icons"> <ul class="icons">
</ul> </ul>

@ -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>

@ -7,23 +7,20 @@
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li> <li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul> </ul>
</nav> </nav>
<p>Authenticated: {{ current_user.is_authenticated }}</p>
{% if current_user.is_authenticated %}
<p>Username: {{ current_user.username }}</p>
<p>Email: {{ current_user.email }}</p>
<p>Password: {{ current_user.password }}</p>
{% endif %}
</header> </header>
<hr>
<script src="/static/js/noty.js"></script> <script src="/static/js/noty.js"></script>
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<script type="text/javascript"> <script type="text/javascript">
{% for message in messages %} {% for category, message in messages %}
{% if category == "message" %}
{% set _c = "info" %}
{% else %}
{% set _c = category %}
{% endif %}
new Noty({ new Noty({
type: 'error', type: '{{ _c }}',
theme: 'relax', theme: 'relax',
layout: 'topCenter', layout: 'topCenter',
text: '{{ message }}', text: '{{ message }}',

@ -1,40 +1,35 @@
<!DOCTYPE HTML> {% extends 'includes/base.html' %}
<html>
{% block content %}
{% include 'includes/head.html' %}
{% if current_user.is_authenticated %}
<body class="is-preload landing"> {% if 2 not in current_user.roles %}
<div id="page-wrapper"> <a href="{{ url_for('creator.join') }}">Become a Creator</a>
{% else %}
{% include 'includes/header.html' %} <a href="{{ url_for('creator.manage_subscriptions') }}">Manage Subscriptions</a>
{% endif %}
{% if feed %} {% endif %}
{% for k in feed %}
<h2>{{ k }}</h2> {% if feed %}
{% if k == 'new_creators' %} <h1>Feed</h1>
<p><a href="{{ url_for('creator.show', username=feed[k]) }}">{{ feed[k] }}</a></p> {% if feed['new_creators'] %}
{% else %} <h2>New Creators</h2>
<p>{{ feed[k]}}</p> {% for c in feed['new_creators'] %}
{% endif %} <p><a href="{{ url_for('creator.show', handle=c.handle) }}">{{ c.handle }}</a></p>
{% endfor %} {% endfor %}
{% else %} {% endif %}
{% if feed['new_posts'] %}
<section id="banner"> <h2>New Posts</h2>
<div class="content"> {% for p in feed['new_posts'] %}
<header> <p>{{ p.id }}</p>
<p>This is a simple prototype and is under heavy development.</p> {% endfor %}
</header> {% endif %}
<span class="image"><img src="/static/images/monero-logo.png" width=150px></span> {% if feed['active_subscriptions'] %}
</div> <h2>Active Subscriptions</h2>
</section> {% for s in feed['active_subscriptions'] %}
<p>{{ s.id }}</p>
{% endif %} {% endfor %}
{% endif %}
{% include 'includes/footer.html' %} {% endif %}
</div> {% endblock %}
{% include 'includes/scripts.html' %}
</body>
</html>

Loading…
Cancel
Save