Compare commits

..

No commits in common. 'revamp' and 'main' have entirely different histories.
revamp ... main

@ -1,7 +1,7 @@
FROM ubuntu:22.04 FROM ubuntu:21.10
ENV MONERO_HASH 937dfcc48d91748dd2e8f58714dfc45d17a0959dff33fc7385bbe06344ff2c16 ENV MONERO_HASH 59e16c53b2aff8d9ab7a8ba3279ee826ac1f2480fbb98e79a149e6be23dd9086
ENV MONERO_DL_URL https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.1.1.tar.bz2 ENV MONERO_DL_URL https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.2.0.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,20 +10,14 @@ 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 QUART_RELOAD=true .venv/bin/quart run QUART_APP=app.py QUART_SECRETS=config.py QUART_DEBUG=1 QUART_ENV=development .venv/bin/quart run

@ -1,6 +1,2 @@
# xmrbackers # lza-quart-app
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,16 +1,7 @@
#!/usr/bin/env python3 from xmrbackers.factory import create_app
from os import environ
from xmrbackers import create_app
app = create_app() app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
# print('running') app.run()
# 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="${3}" export DAEMON_ADDRESS=singapore.node.xmr.pm
# 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 ${DAEMON_ADDRESS} \ --daemon-address http://${DAEMON_ADDRESS}:${PORT} \
--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 ${DAEMON_ADDRESS} \ --daemon-address http://${DAEMON_ADDRESS}:${PORT} \
--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:${XMR_WALLET_RPC_PORT:-8000}:${XMR_WALLET_RPC_PORT:-8000} - 127.0.0.1:8000:8000
volumes: volumes:
- ${DATA_DIR:-./data/wallet}:/data - ./data/wallet:/data
command: command:
bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}" "${XMR_DAEMON_URI}" bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}"

@ -3,11 +3,11 @@ DB_USER=xmrbackers
DB_NAME=xmrbackers DB_NAME=xmrbackers
DB_HOST=localhost DB_HOST=localhost
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_ENDPOINT=http://localhost:9090
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,8 +9,9 @@ qrcode
redis redis
peewee peewee
requests requests
SQLAlchemy
WTForms WTForms
quart==0.17.0 quart
monero monero
arrow arrow
flake8 flake8

@ -1,16 +0,0 @@
# 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

@ -1 +0,0 @@
from xmrbackers.factory import create_app

@ -1,9 +1,8 @@
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()
@ -12,21 +11,13 @@ 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_PORT = getenv('XMR_WALLET_RPC_PORT', 8000) XMR_WALLET_RPC_ENDPOINT = getenv('XMR_WALLET_RPC_ENDPOINT')
XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK') XMR_DAEMON_URI = getenv('XMR_DAEMON_URI')
# Database # Database
DB_HOST = getenv('DB_HOST', 'localhost') DB_HOST = getenv('DB_HOST', 'localhost')
@ -40,7 +31,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', 300)) SESSION_LENGTH = int(getenv('SESSION_LENGTH', 30))
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,8 +1,10 @@
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):
@ -18,10 +20,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 api, auth, creator, post, main from xmrbackers.routes import meta, api, auth, creator, post
from xmrbackers import filters from xmrbackers import filters
await _setup_db(app) await _setup_db(app)
app.register_blueprint(main.bp) app.register_blueprint(meta.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)
@ -39,3 +41,5 @@ 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
from monero import numbers import monero
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 numbers.from_atomic(amt) return monero.numbers.from_atomic(amt)

@ -1,46 +1,14 @@
# import quart.flask_patch
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, FloatField from wtforms import StringField
from wtforms.validators import DataRequired, ValidationError from wtforms.validators import DataRequired
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"})
def is_valid_xmr_address(form, field): class ConfirmSubscription(FlaskForm):
try: tx_id = StringField('TX ID:', validators=[DataRequired()], render_kw={"placeholder": "TX ID", "class": "form-control", "type": "text"})
# Ensure the provided address is valid address/subaddress/integrated address tx_key = StringField('TX Key:', validators=[DataRequired()], render_kw={"placeholder": "TX Key", "class": "form-control", "type": "text"})
a = address(field.data) wallet_address = StringField('XMR Address:', validators=[DataRequired()], render_kw={"placeholder": "XMR Address", "class": "form-control", "type": "text"})
# 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,90 +1,17 @@
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:
w = Wallet( check_data = {
port=config.XMR_WALLET_RPC_PORT, 'txid': tx_id,
user=config.XMR_WALLET_RPC_USER, 'tx_key': tx_key,
password=config.XMR_WALLET_RPC_PASS, 'address': wallet_address
timeout=3 }
) w = Wallet(port=8000, user=config.XMR_WALLET_RPC_USER, password=config.XMR_WALLET_RPC_PASS)
res = w._backend.raw_request(method, data) res = w._backend.raw_request('check_tx_key', check_data)
return res return res
except Exception as e: except:
raise e raise Exception('there was a problem i dont feel like writing good code for right now')
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,13 +1,8 @@
from datetime import datetime, timedelta from datetime import datetime
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(
@ -17,44 +12,16 @@ 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 base user management and reporting. User model is for pure user authentication management
and reporting.
""" """
id = pw.AutoField() id = pw.AutoField()
register_date = pw.DateTimeField(default=datetime.utcnow) register_date = pw.DateTimeField(default=datetime.now)
last_login_date = pw.DateTimeField(default=datetime.utcnow) last_login_date = pw.DateTimeField(default=datetime.now)
handle = pw.CharField(unique=True) username = pw.CharField(unique=True)
wallet_address = pw.CharField(unique=True) password = pw.CharField()
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):
@ -70,117 +37,46 @@ class User(pw.Model):
@property @property
def is_admin(self): def is_admin(self):
return UserRole.admin in self.roles return self.admin
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 Profile(pw.Model): class CreatorProfile(pw.Model):
""" """
Profile model is for users to provide metadata about CreatorProfile model is for creators to provide metadata about
themselves; Creators for their fans or even just the general public. themselves 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.utcnow) create_date = pw.DateTimeField(default=datetime.now)
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)
bio = pw.CharField(null=True) email = pw.CharField(unique=True, null=True)
bio = pw.CharField()
verified = pw.CharField(default=False) verified = pw.CharField(default=False)
class Meta: class Meta:
database = db database = db
class Transaction(pw.Model): class BackerProfile(pw.Model):
"""
Transaction model is a simple reference to a Monero transaction so that we can track
which transactions have occurred on-chain and which subscription they correspond to.
"""
id = pw.AutoField()
tx_id = pw.CharField(unique=True)
atomic_xmr = pw.BigIntegerField()
to_address = pw.CharField()
class Meta:
database = db
class CreatorSubscription(pw.Model):
""" """
CreatorSubscription model is for tracking subscriptions of creators to the platform. BackerProfile model is for backers to give contact info
Subscription is a flat fee with a modifier based on usage/consumption if they wanted to retain communications in some way...ie
and will be re-negotiated every N days. recurring emails and/or notifications. For now.
""" """
id = pw.AutoField() id = pw.AutoField()
user = pw.ForeignKeyField(User) user = pw.ForeignKeyField(User, backref='backer_profile')
tx = pw.ForeignKeyField(Transaction) register_date = pw.DateTimeField(default=datetime.now)
create_date = pw.DateTimeField(default=datetime.utcnow) last_login_date = pw.DateTimeField(default=datetime.now)
atomic_xmr = pw.BigIntegerField() email = pw.CharField(unique=True, null=True)
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
@ -189,21 +85,20 @@ class CreatorSubscription(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/backers. There is no their subscription plan to release for subscribers. 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()
user = pw.ForeignKeyField(User) create_date = pw.DateTimeField(default=datetime.now)
create_date = pw.DateTimeField(default=datetime.utcnow) creator = pw.ForeignKeyField(CreatorProfile)
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_active_subscriptions(self): def get_end_date(self) -> datetime:
return Subscription.select().where( # some timedelta shiz
Subscription.meta == self pass
)
class Meta: class Meta:
database = db database = db
@ -211,56 +106,33 @@ 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 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. Once a subscription is in place and is
is in place and is associated with a user, that user is then elligible to associated with a user, that user is then elligible to view that
view that creator's content until the subscription expires (number_hours). creator's content.
""" """
id = pw.AutoField() id = pw.AutoField()
subscribe_date = pw.DateTimeField(default=datetime.utcnow) subscribe_date = pw.DateTimeField(default=datetime.now)
tx = pw.ForeignKeyField(Transaction) active = pw.BooleanField(default=True)
creator = pw.ForeignKeyField(User) creator = pw.ForeignKeyField(CreatorProfile)
backer = pw.ForeignKeyField(User) backer = pw.ForeignKeyField(BackerProfile)
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):
""" """
Post model represents a post from a creator consisting of Content objects TextPost model is the first content type available to post. Metadata
which is only viewable by backers with an active subscription. here is basic for now, let's proof out the other components first.
""" """
id = pw.AutoField() id = pw.AutoField()
post_date = pw.DateTimeField(default=datetime.utcnow) post_date = pw.DateTimeField(default=datetime.now)
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.utcnow) last_edit_date = pw.DateTimeField(default=datetime.now)
creator = pw.ForeignKeyField(User) creator = pw.ForeignKeyField(CreatorProfile)
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.forms import UserLogin, UserRegistration, UserChallenge from xmrbackers.factory import bcrypt
from xmrbackers.helpers import make_wallet_rpc from xmrbackers.forms import UserAuth
from xmrbackers.models import User from xmrbackers.models import User
@ -11,87 +11,60 @@ bp = Blueprint('auth', 'auth')
@bp.route("/register", methods=["GET", "POST"]) @bp.route("/register", methods=["GET", "POST"])
async def register(): async def register():
form = UserRegistration() form = UserAuth()
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('main.index')) return redirect(url_for('meta.index'))
if form.validate_on_submit(): if form.validate_on_submit():
# Check if handle already exists # Check if username already exists
user = User.select().where( user = User.select().where(
User.handle == form.handle.data User.username == form.username.data
).first() ).first()
if user: if user:
await flash('This handle is already registered.') await flash('This username 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(
handle=form.handle.data, username=form.username.data,
wallet_address=form.wallet_address.data, password=bcrypt.generate_password_hash(form.password.data).decode('utf8'),
) )
user.save() user.save()
login_user(user) login_user(user)
return redirect(url_for('main.index')) return redirect(url_for('meta.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 = UserLogin() form = UserAuth()
if current_user.is_authenticated: if current_user.is_authenticated:
await flash('Already logged in.') await flash('Already logged in.')
return redirect(url_for('main.index')) return redirect(url_for('meta.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.handle == form.handle.data User.username == form.username.data
).first() ).first()
if not user: if not user:
await flash('That handle does not exist.') await flash('Invalid username or password.')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
return redirect(url_for('auth.challenge', handle=user.handle))
return await render_template("auth/login.html", form=form) # Check if password is correct
password_matches = bcrypt.check_password_hash(
@bp.route("/login/challenge/<handle>", methods=["GET", "POST"]) user.password,
async def challenge(handle): form.password.data
form = UserChallenge() )
user = User.select().where(User.handle == handle).first() if not password_matches:
if not user: await flash('Invalid username or password.')
await flash('User does not exist.') return redirect(url_for('auth.login'))
return redirect(url_for('main.index'))
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) login_user(user)
await flash('Successful login!') return redirect(url_for('meta.index'))
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( return await render_template("auth/login.html", form=form)
'auth/challenge.html',
user=user,
form=form
)
@bp.route("/logout") @bp.route("/logout")
async def logout(): async def logout():
@ -99,4 +72,30 @@ async def logout():
logout_user() logout_user()
else: else:
await flash('Not authenticated!') await flash('Not authenticated!')
return redirect(url_for('main.index')) return redirect(url_for('meta.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,230 +1,100 @@
from quart import Blueprint, render_template, flash, redirect, url_for, request 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 monero.backends.jsonrpc.exceptions import RPCError
from xmrbackers.models import * from xmrbackers.forms import ConfirmSubscription
from xmrbackers.helpers import make_wallet_rpc from xmrbackers.models import User, CreatorProfile, BackerProfile, TextPost
from xmrbackers import config, forms from xmrbackers.models import Subscription, SubscriptionMeta
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 = User.select().join(Profile).where( creators = CreatorProfile.select().order_by(
User.roles.contains_any(UserRole.creator) CreatorProfile.create_date.desc()
).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('/creators/join', methods=['GET', 'POST']) @bp.route('/creator/<username>')
@login_required async def show(username):
async def join(): user = User.select().where(User.username == username)
form = forms.ConfirmPlatformSubscription() creator = CreatorProfile.select().where(
valid_address = False CreatorProfile.user == user
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() ).first()
for i in ['confirmations', 'in_pool', 'received']: if creator:
assert i in res posts = TextPost.select().where(
if res['in_pool']: TextPost.creator == creator,
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') TextPost.hidden == False
elif res['received'] < to_atomic(config.CREATOR_SUBSCRIPTION_FEE_XMR): ).order_by(TextPost.post_date.desc())
await flash(f'Not enought XMR sent. {from_atomic(res["received"])} XMR sent, expected {config.CREATOR_SUBSCRIPTION_FEE_XMR} XMR.', 'error') return await render_template(
elif existing_tx: 'creator/creator.html',
if existing_sub: creator=creator,
await flash('This transaction was already used for another subscription.', 'warning') posts=posts
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: else:
await flash('Something went wrong. No idea what, though. Check with admin.', 'error') await flash('That creator does not exist.')
except Exception as e: return redirect(url_for('meta.index'))
await flash(f'seems bad: {e}', 'error')
@bp.route('/creator/<username>/subscription')
async def subscription(username):
user = User.select().where(User.username == username)
creator = CreatorProfile.select().where(
CreatorProfile.user == user
)
if creator:
subscription_meta = SubscriptionMeta.select().where(
SubscriptionMeta.creator == creator
).order_by(SubscriptionMeta.create_date.desc()).first()
form = ConfirmSubscription()
return await render_template( return await render_template(
'creator/join.html', 'creator/subscription.html',
subscription_meta=subscription_meta,
form=form form=form
) )
else:
await flash('That creator does not exist.')
@bp.route('/creator/<handle>', methods=['GET', 'POST']) return redirect(url_for('meta.index'))
async def show(handle):
form = forms.ConfirmCreatorSubscription() @bp.route('/subscription/<int:subscription_id>/confirm', methods=['POST'])
creator = User.select().join(Profile, pw.JOIN.LEFT_OUTER).where( async def confirm_subscription(subscription_id):
User.handle == handle, # do checks here for SubscriptionMeta assumption
User.roles.contains_any(UserRole.creator) sm = SubscriptionMeta.get_or_none(subscription_id)
).first() form = ConfirmSubscription()
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(): if form.validate_on_submit():
data = { w = Wallet(
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 = make_wallet_rpc('check_tx_key', data) res = w._backend.raw_request('check_tx_key', check_data)
subs = subscriptions.where( except:
SubscriptionMeta.wallet_address == data['address'] await flash(f'Invalid transaction! No subscription for you!')
) return redirect(url_for('creator.show', username=sm.creator.user.username))
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(): if res['received'] >= sm.atomic_xmr:
s = SubscriptionMeta( backer_profile = BackerProfile.select().where(
user=current_user, BackerProfile.user == current_user
atomic_xmr=to_atomic(form.price_xmr.data), ).first()
number_hours=form.number_days.data * 24.0, s = Subscription(
wallet_address=current_user.wallet_address creator=sm.creator.id,
backer=backer_profile.id,
meta=sm.id,
) )
s.save() s.save()
await flash('posting form data', 'success') 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))
return await render_template( else:
'creator/manage_subscriptions.html', await flash('Not enough XMR sent! No subscription for you!')
platform_subs=platform_subs, return redirect(url_for('creator.show', username=sm.creator.user.username))
content_subs=content_subs, else:
subscribers=subscribers, await flash('Unable to accept form POST.')
form=form 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 = 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'))

@ -1,30 +0,0 @@
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
)

@ -0,0 +1,30 @@
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 Content, Subscription from xmrbackers.models import TextPost, 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'))

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

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

@ -0,0 +1,30 @@
<!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>

@ -1,17 +0,0 @@
{% 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 %}

@ -1,49 +0,0 @@
{% 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 %}

@ -1,38 +0,0 @@
{% 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 %}

@ -1,19 +0,0 @@
<!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>

@ -1,9 +0,0 @@
{% 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,14 +1,3 @@
<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>

@ -1,18 +0,0 @@
<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,20 +7,23 @@
<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_categories=true) %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
<script type="text/javascript"> <script type="text/javascript">
{% for category, message in messages %} {% for message in messages %}
{% if category == "message" %}
{% set _c = "info" %}
{% else %}
{% set _c = category %}
{% endif %}
new Noty({ new Noty({
type: '{{ _c }}', type: 'error',
theme: 'relax', theme: 'relax',
layout: 'topCenter', layout: 'topCenter',
text: '{{ message }}', text: '{{ message }}',

@ -1,35 +1,40 @@
{% extends 'includes/base.html' %} <!DOCTYPE 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 %}
{% endif %}
{% if feed %} {% if feed %}
<h1>Feed</h1> {% for k in feed %}
{% if feed['new_creators'] %} <h2>{{ k }}</h2>
<h2>New Creators</h2> {% if k == 'new_creators' %}
{% for c in feed['new_creators'] %} <p><a href="{{ url_for('creator.show', username=feed[k]) }}">{{ feed[k] }}</a></p>
<p><a href="{{ url_for('creator.show', handle=c.handle) }}">{{ c.handle }}</a></p> {% else %}
{% endfor %} <p>{{ feed[k]}}</p>
{% endif %}
{% if feed['new_posts'] %}
<h2>New Posts</h2>
{% for p in feed['new_posts'] %}
<p>{{ p.id }}</p>
{% endfor %}
{% endif %} {% endif %}
{% if feed['active_subscriptions'] %}
<h2>Active Subscriptions</h2>
{% for s in feed['active_subscriptions'] %}
<p>{{ s.id }}</p>
{% endfor %} {% 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 %} {% endif %}
{% endif %}
{% endblock %} {% include 'includes/footer.html' %}
</div>
{% include 'includes/scripts.html' %}
</body>
</html>

Loading…
Cancel
Save