From 5e50fd1640c98dcc59853a509b31bdd5559baf7d Mon Sep 17 00:00:00 2001 From: lza_menace Date: Sun, 27 Nov 2022 11:59:18 -0800 Subject: [PATCH] init --- .gitignore | 4 + Dockerfile-monero | 38 ++++ LICENSE | 21 +++ Makefile | 33 ++++ README.md | 7 + bin/run_wallet.sh | 38 ++++ docker-compose.yaml | 16 ++ env-example | 12 ++ manage.sh | 31 ++++ nerochan/__init__.py | 1 + nerochan/app.py | 10 ++ nerochan/cli.py | 12 ++ nerochan/config.py | 35 ++++ nerochan/decorators.py | 25 +++ nerochan/factory.py | 38 ++++ nerochan/filters.py | 25 +++ nerochan/forms.py | 47 +++++ nerochan/helpers.py | 19 ++ nerochan/library/__init__.py | 0 nerochan/library/cache.py | 35 ++++ nerochan/library/market.py | 16 ++ nerochan/models.py | 103 +++++++++++ nerochan/routes/__init__.py | 0 nerochan/routes/api.py | 11 ++ nerochan/routes/auth.py | 102 +++++++++++ nerochan/routes/main.py | 30 ++++ nerochan/routes/post.py | 27 +++ nerochan/static/css/main.css | 0 nerochan/static/css/noty-relax.css | 46 +++++ nerochan/static/css/noty.css | 222 ++++++++++++++++++++++++ nerochan/static/css/noty.css.map | 1 + nerochan/static/images/monero-logo.png | Bin 0 -> 11912 bytes nerochan/templates/auth/challenge.html | 11 ++ nerochan/templates/auth/login.html | 8 + nerochan/templates/auth/register.html | 8 + nerochan/templates/includes/base.html | 18 ++ nerochan/templates/includes/debug.html | 9 + nerochan/templates/includes/footer.html | 18 ++ nerochan/templates/includes/form.html | 18 ++ nerochan/templates/includes/head.html | 22 +++ nerochan/templates/includes/header.html | 20 +++ nerochan/templates/index.html | 9 + nerochan/templates/post/show.html | 25 +++ requirements.txt | 17 ++ 44 files changed, 1188 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile-monero create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 bin/run_wallet.sh create mode 100644 docker-compose.yaml create mode 100644 env-example create mode 100755 manage.sh create mode 100644 nerochan/__init__.py create mode 100755 nerochan/app.py create mode 100644 nerochan/cli.py create mode 100644 nerochan/config.py create mode 100644 nerochan/decorators.py create mode 100644 nerochan/factory.py create mode 100644 nerochan/filters.py create mode 100644 nerochan/forms.py create mode 100644 nerochan/helpers.py create mode 100644 nerochan/library/__init__.py create mode 100644 nerochan/library/cache.py create mode 100644 nerochan/library/market.py create mode 100644 nerochan/models.py create mode 100644 nerochan/routes/__init__.py create mode 100644 nerochan/routes/api.py create mode 100644 nerochan/routes/auth.py create mode 100644 nerochan/routes/main.py create mode 100644 nerochan/routes/post.py create mode 100644 nerochan/static/css/main.css create mode 100644 nerochan/static/css/noty-relax.css create mode 100644 nerochan/static/css/noty.css create mode 100644 nerochan/static/css/noty.css.map create mode 100644 nerochan/static/images/monero-logo.png create mode 100644 nerochan/templates/auth/challenge.html create mode 100644 nerochan/templates/auth/login.html create mode 100644 nerochan/templates/auth/register.html create mode 100644 nerochan/templates/includes/base.html create mode 100644 nerochan/templates/includes/debug.html create mode 100644 nerochan/templates/includes/footer.html create mode 100644 nerochan/templates/includes/form.html create mode 100644 nerochan/templates/includes/head.html create mode 100644 nerochan/templates/includes/header.html create mode 100644 nerochan/templates/index.html create mode 100644 nerochan/templates/post/show.html create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a607f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv +.env +data +__pycache__ diff --git a/Dockerfile-monero b/Dockerfile-monero new file mode 100644 index 0000000..d61ada7 --- /dev/null +++ b/Dockerfile-monero @@ -0,0 +1,38 @@ +FROM ubuntu:22.04 + +ENV MONERO_HASH 937dfcc48d91748dd2e8f58714dfc45d17a0959dff33fc7385bbe06344ff2c16 +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_SUMS_FILE sha256sums + +WORKDIR /opt/monero + +# Update system and install dependencies +# Download Monero binaries from Github +# Confirm hashes match +# Install binaries to system path +# Clean up + +RUN apt-get update +RUN apt-get upgrade -y +RUN apt-get install -y tar wget bzip2 + +RUN wget -qO ${MONERO_DL_FILE} ${MONERO_DL_URL} +RUN echo "${MONERO_HASH} ${MONERO_DL_FILE}" > ${MONERO_SUMS_FILE} \ + && sha256sum -c ${MONERO_SUMS_FILE}; \ + if [ "$?" -eq 0 ]; \ + then \ + echo -e "[+] Hashes match - proceeding with container build"; \ + else \ + echo -e "[!] Hashes do not match - exiting"; \ + exit 5; \ + fi \ + && mkdir ./tmp \ + && tar xvf ${MONERO_DL_FILE} -C ./tmp --strip 1 \ + && mv ./tmp/* /usr/local/bin/ \ + && rm -rf ./tmp ${MONERO_SUMS_FILE} ${MONERO_DL_FILE} + +WORKDIR /tmp +COPY bin/run_wallet.sh /run_wallet.sh + +WORKDIR /data diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29da2e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 @lza_menace (https://lzahq.tech) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e8ab3aa --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: format help + +# Help system from https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +.DEFAULT_GOAL := help + +help: + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +setup: ## Establish local environment with dependencies installed + python3 -m venv .venv + .venv/bin/pip install -r requirements.txt + mkdir -p data/uploads + +build: ## Build containers + docker-compose build + +up: ## Start containers + docker-compose up -d + +down: ## Stop containers + docker-compose down + +dbshell: + docker-compose exec db psql -U nerochan + +shell: ## Start Flask CLI shell + ./manage.sh shell + +init: ## Initialize SQLite DB + ./manage.sh init + +dev: ## Start Flask development web server + ./manage.sh run diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a2fa0a --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# nerochan + +Post your best neroChans. Get tipped. + +No middle-men. p2p tips. + +No passwords. Wallet signing authentication. diff --git a/bin/run_wallet.sh b/bin/run_wallet.sh new file mode 100644 index 0000000..8fbb371 --- /dev/null +++ b/bin/run_wallet.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +export RPC_CREDS="${2}" +export DAEMON_ADDRESS="${3}" + +# Define the network we plan to operate our wallet in +if [[ "${1}" == "stagenet" ]]; then + export NETWORK=--stagenet + export PORT=38081 +elif [[ "${1}" == "testnet" ]]; then + export NETWORK=--testnet + export PORT=28081 +else + export NETWORK= + export PORT=18089 +fi + +# Create new wallet if it doesn't exist +if [[ ! -d /data/wallet ]]; then + monero-wallet-cli ${NETWORK} \ + --generate-new-wallet /data/wallet \ + --daemon-address ${DAEMON_ADDRESS} \ + --trusted-daemon \ + --use-english-language-names \ + --mnemonic-language English +fi + +# Run RPC wallet +monero-wallet-rpc ${NETWORK} \ + --daemon-address ${DAEMON_ADDRESS} \ + --wallet-file /data/wallet \ + --password "" \ + --rpc-login ${RPC_CREDS} \ + --rpc-bind-port 8000 \ + --rpc-bind-ip 0.0.0.0 \ + --confirm-external-bind \ + --log-file /data/wallet-rpc.log \ + --trusted-daemon diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..8e4693e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +version: '3' +services: + cache: + image: redis:latest + ports: + - 127.0.0.1:6379:6379 + wallet: + build: + context: . + dockerfile: Dockerfile-monero + ports: + - 127.0.0.1:${XMR_WALLET_RPC_PORT:-8000}:${XMR_WALLET_RPC_PORT:-8000} + volumes: + - ${DATA_DIR:-./data/wallet}:/data + command: + bash /run_wallet.sh "${XMR_WALLET_NETWORK}" "${XMR_WALLET_RPC_USER}:${XMR_WALLET_RPC_PASS}" "${XMR_DAEMON_URI}" diff --git a/env-example b/env-example new file mode 100644 index 0000000..e00c872 --- /dev/null +++ b/env-example @@ -0,0 +1,12 @@ +DB_PATH=./data/sqlite.db + +XMR_WALLET_RPC_USER=xxxxxxxxxx +XMR_WALLET_RPC_PASS=xxxxxxxxxxxxxxxxxxx +XMR_WALLET_RPC_PORT=8000 +XMR_DAEMON_URI=http://super.fast.node.xmr.pm:38089 +XMR_WALLET_NETWORK=stagenet + +SITE_NAME=nerochan +SECRET_KEY=xxxxxxxxxxxxxxxxxxx +STATS_TOKEN=xxxxxxxxxxxxxxxxxxxx +SERVER_NAME=127.0.0.1:5000 diff --git a/manage.sh b/manage.sh new file mode 100755 index 0000000..6d351fa --- /dev/null +++ b/manage.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +source .venv/bin/activate +export FLASK_APP=nerochan/app.py +export FLASK_SECRETS=config.py +export FLASK_DEBUG=1 +export FLASK_ENV=development + +# override +source .env + +if [[ ${1} == "prod" ]]; +then + export FLASK_DEBUG=0 + export FLASK_ENV=production + export BASE=./data/gunicorn + mkdir -p $BASE + pgrep -F $BASE/gunicorn.pid + if [[ $? != 0 ]]; then + gunicorn \ + --bind 127.0.0.1:4000 "nerochan.app:app" \ + --daemon \ + --log-file $BASE/gunicorn.log \ + --pid $BASE/gunicorn.pid \ + --reload + sleep 2 + echo "Started gunicorn on 127.0.0.1:4000 with pid $(cat $BASE/gunicorn.pid)" + fi +else + flask $@ +fi diff --git a/nerochan/__init__.py b/nerochan/__init__.py new file mode 100644 index 0000000..be1e109 --- /dev/null +++ b/nerochan/__init__.py @@ -0,0 +1 @@ +from nerochan.factory import create_app \ No newline at end of file diff --git a/nerochan/app.py b/nerochan/app.py new file mode 100755 index 0000000..7444507 --- /dev/null +++ b/nerochan/app.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +from nerochan import create_app + + +app = create_app() + + +if __name__ == '__main__': + app.run(debug=True, use_reloader=True) diff --git a/nerochan/cli.py b/nerochan/cli.py new file mode 100644 index 0000000..f2b28ea --- /dev/null +++ b/nerochan/cli.py @@ -0,0 +1,12 @@ +import click + + +def cli(app): + @app.cli.command('init') + def init(): + import peewee + from nerochan.models import db + model = peewee.Model.__subclasses__() + db.create_tables(model) + + return app diff --git a/nerochan/config.py b/nerochan/config.py new file mode 100644 index 0000000..ffc5cd4 --- /dev/null +++ b/nerochan/config.py @@ -0,0 +1,35 @@ +from secrets import token_urlsafe +from datetime import timedelta +from os import getenv + +from dotenv import load_dotenv + + +load_dotenv() + +# Site meta +SITE_NAME = getenv('SITE_NAME', 'nerochan') +SECRET_KEY = getenv('SECRET_KEY', token_urlsafe(12)) +SERVER_NAME = getenv('SERVER_NAME', '127.0.0.1:5000') + +# Crypto RPC +XMR_WALLET_PASS = getenv('XMR_WALLET_PASS') +XMR_WALLET_RPC_USER = getenv('XMR_WALLET_RPC_USER') +XMR_WALLET_RPC_PASS = getenv('XMR_WALLET_RPC_PASS') +XMR_WALLET_RPC_PORT = getenv('XMR_WALLET_RPC_PORT', 8000) +XMR_WALLET_NETWORK = getenv('XMR_WALLET_NETWORK') + +# Database +DB_PATH = getenv('DB_PATH', './data/sqlite.db') + +# Redis +REDIS_HOST = getenv('REDIS_HOST', 'localhost') +REDIS_PORT = getenv('REDIS_PORT', 6379) + +# Sessions +SESSION_LENGTH = int(getenv('SESSION_LENGTH', 300)) +PERMANENT_SESSION_LIFETIME = timedelta(minutes=SESSION_LENGTH) +MAX_CONTENT_LENGTH = 50 * 1024 * 1024 + +# Development +TEMPLATES_AUTO_RELOAD = True diff --git a/nerochan/decorators.py b/nerochan/decorators.py new file mode 100644 index 0000000..409128e --- /dev/null +++ b/nerochan/decorators.py @@ -0,0 +1,25 @@ +from flask import session, redirect, url_for, flash +from flask_login import current_user +from functools import wraps +from nerochan.models import User, CreatorProfile, BackerProfile, Subscription + + +# def login_required(f): +# @wraps(f) +# def decorated_function(*args, **kwargs): +# if "auth" not in session or not session["auth"]: +# return redirect(url_for("auth.login")) +# return f(*args, **kwargs) +# return decorated_function + +def subscription_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + print(current_user) + # m = Moderator.filter(username=session["auth"]["preferred_username"]) + # if m: + # return f(*args, **kwargs) + # else: + # flash("You are not a moderator") + # return redirect(url_for("index")) + return decorated_function diff --git a/nerochan/factory.py b/nerochan/factory.py new file mode 100644 index 0000000..dba09be --- /dev/null +++ b/nerochan/factory.py @@ -0,0 +1,38 @@ +from flask import Flask +from flask_login import LoginManager + +from nerochan.cli import cli + + +def setup_db(app: Flask): + import peewee + models = peewee.Model.__subclasses__() + for m in models: + m.create_table() + + +def create_app(): + app = Flask(__name__) + app = cli(app) + app.config.from_envvar('FLASK_SECRETS') + + # Login manager + login_manager = LoginManager(app) + login_manager.login_view = 'auth.login' + login_manager.logout_view = 'auth.logout' + + @login_manager.user_loader + def load_user(user_id): + from nerochan.models import User + return User.get_or_none(user_id) + + with app.app_context(): + from nerochan.routes import api, auth, post, main + from nerochan import filters + app.register_blueprint(main.bp) + app.register_blueprint(api.bp) + app.register_blueprint(auth.bp) + app.register_blueprint(post.bp) + app.register_blueprint(filters.bp) + + return app diff --git a/nerochan/filters.py b/nerochan/filters.py new file mode 100644 index 0000000..ec03734 --- /dev/null +++ b/nerochan/filters.py @@ -0,0 +1,25 @@ +from datetime import datetime + +import arrow +from monero import numbers +from flask import Blueprint, current_app + + +bp = Blueprint('filters', 'filters') + + +@bp.app_template_filter('humanize') +def humanize(d): + return arrow.get(d).humanize() + +@bp.app_template_filter('ts') +def from_ts(v): + return datetime.fromtimestamp(v) + +@bp.app_template_filter('xmr_block_explorer') +def xmr_block_explorer(v): + return f'https://www.exploremonero.com/transaction/{v}' + +@bp.app_template_filter('from_atomic') +def from_atomic(amt): + return numbers.from_atomic(amt) diff --git a/nerochan/forms.py b/nerochan/forms.py new file mode 100644 index 0000000..e8f3984 --- /dev/null +++ b/nerochan/forms.py @@ -0,0 +1,47 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, FloatField +from wtforms.validators import DataRequired, ValidationError +from monero.address import address + +from nerochan import config + + +def is_valid_xmr_address(form, field): + try: + # Ensure the provided address is valid address/subaddress/integrated address + a = address(field.data) + print(config.XMR_WALLET_NETWORK) + # 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'}) diff --git a/nerochan/helpers.py b/nerochan/helpers.py new file mode 100644 index 0000000..4f4ad13 --- /dev/null +++ b/nerochan/helpers.py @@ -0,0 +1,19 @@ +import peewee as pw +import playhouse.postgres_ext as pwpg +from monero.wallet import Wallet + +from nerochan import config + + +def make_wallet_rpc(method, data={}): + try: + w = Wallet( + port=config.XMR_WALLET_RPC_PORT, + user=config.XMR_WALLET_RPC_USER, + password=config.XMR_WALLET_RPC_PASS, + timeout=3 + ) + res = w._backend.raw_request(method, data) + return res + except Exception as e: + raise e diff --git a/nerochan/library/__init__.py b/nerochan/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nerochan/library/cache.py b/nerochan/library/cache.py new file mode 100644 index 0000000..6dc9d37 --- /dev/null +++ b/nerochan/library/cache.py @@ -0,0 +1,35 @@ +from json import loads as json_loads +from json import dumps as json_dumps +from datetime import timedelta + +from redis import Redis + +from nerochan.library.market import get_market_data +from nerochan import config + + +class Cache(object): + def __init__(self): + self.redis = Redis(host=config.REDIS_HOST, port=config.REDIS_PORT) + + def store_data(self, item_name, expiration_minutes, data): + self.redis.setex( + item_name, + timedelta(minutes=expiration_minutes), + value=data + ) + + def get_coin_price(self, coin_name): + key_name = f'{coin_name}_price' + data = self.redis.get(key_name) + if data: + return json_loads(data) + else: + d = get_market_data(coin_name) + data = { + key_name: d['market_data']['current_price'], + } + self.store_data(key_name, 4, json_dumps(data)) + return data + +cache = Cache() diff --git a/nerochan/library/market.py b/nerochan/library/market.py new file mode 100644 index 0000000..1763905 --- /dev/null +++ b/nerochan/library/market.py @@ -0,0 +1,16 @@ +from requests import get as r_get + + +def get_market_data(coin_name="monero"): + data = { + 'localization': False, + 'tickers': False, + 'market_data': True, + 'community_data': False, + 'developer_data': False, + 'sparkline': False + } + headers = {'accept': 'application/json'} + url = f'https://api.coingecko.com/api/v3/coins/{coin_name}' + r = r_get(url, headers=headers, data=data) + return r.json() diff --git a/nerochan/models.py b/nerochan/models.py new file mode 100644 index 0000000..f753a55 --- /dev/null +++ b/nerochan/models.py @@ -0,0 +1,103 @@ +from datetime import datetime +from secrets import token_urlsafe + +import peewee as pw + +from nerochan import config + + +db = pw.SqliteDatabase( + config.DB_PATH +) + +def gen_challenge(): + return token_urlsafe().replace('-', '').replace('_', '') + + +class User(pw.Model): + """ + User model is for base user management and reporting. + """ + id = pw.AutoField() + register_date = pw.DateTimeField(default=datetime.utcnow) + last_login_date = pw.DateTimeField(default=datetime.utcnow) + handle = pw.CharField(unique=True) + wallet_address = pw.CharField(unique=True) + challenge = pw.CharField(default=gen_challenge) + is_admin = pw.BooleanField(default=False) + is_mod = pw.BooleanField(default=False) + + @property + def is_authenticated(self): + return True + + @property + def is_active(self): + return True + + @property + def is_anonymous(self): + return False + + def get_id(self): + return self.id + + def regenerate_challenge(self): + self.challenge = gen_challenge() + self.save() + + class Meta: + database = db + + +class Profile(pw.Model): + """ + Profile model is for users to provide metadata about + themselves; Creators for their fans or even just the general public. + Links to social media, contact info, portfolio sites, etc + should go in here. + """ + id = pw.AutoField() + user = pw.ForeignKeyField(User) + create_date = pw.DateTimeField(default=datetime.utcnow) + website = pw.CharField(unique=True, null=True) + twitter_handle = pw.CharField(unique=True, null=True) + bio = pw.CharField(null=True) + email = pw.CharField(unique=True, null=True) + verified = pw.CharField(default=False) + + class Meta: + database = db + + +class Content(pw.Model): + """ + Content model is any uploaded content from a creator. + """ + id = pw.AutoField() + creator = pw.ForeignKeyField(User) + path = pw.CharField() + upload_date = pw.DateTimeField(default=datetime.utcnow) + last_edit_date = pw.DateTimeField(default=datetime.utcnow) + approved = pw.BooleanField(default=False) + hidden = pw.BooleanField(default=False) + title = pw.CharField() + description = pw.TextField() + + class Meta: + database = db + + +class Transaction(pw.Model): + """ + Transaction model is a simple reference to a Monero transaction so that we can track + which transactions have occurred on-chain and what content they correspond to. + """ + id = pw.AutoField() + tx_id = pw.CharField(unique=True) + atomic_xmr = pw.BigIntegerField() + to_address = pw.CharField() + content = pw.ForeignKeyField(Content) + + class Meta: + database = db diff --git a/nerochan/routes/__init__.py b/nerochan/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nerochan/routes/api.py b/nerochan/routes/api.py new file mode 100644 index 0000000..8e1ca0f --- /dev/null +++ b/nerochan/routes/api.py @@ -0,0 +1,11 @@ +from flask import Blueprint, jsonify + + +bp = Blueprint('api', 'api') + +@bp.route('/api/test') +def get_prices(): + return jsonify({ + 'test': True, + 'message': 'This is only a test.' + }) diff --git a/nerochan/routes/auth.py b/nerochan/routes/auth.py new file mode 100644 index 0000000..51e9e2c --- /dev/null +++ b/nerochan/routes/auth.py @@ -0,0 +1,102 @@ +from flask import Blueprint, render_template +from flask import flash, redirect, url_for +from flask_login import login_user, logout_user, current_user + +from nerochan.forms import UserLogin, UserRegistration, UserChallenge +from nerochan.helpers import make_wallet_rpc +from nerochan.models import User + + +bp = Blueprint('auth', 'auth') + +@bp.route("/register", methods=["GET", "POST"]) +def register(): + form = UserRegistration() + if current_user.is_authenticated: + flash('Already registered and authenticated.') + return redirect(url_for('main.index')) + + if form.validate_on_submit(): + # Check if handle already exists + user = User.select().where( + User.handle == form.handle.data + ).first() + if user: + flash('This handle is already registered.') + return redirect(url_for('auth.login')) + + # Save new user + user = User( + handle=form.handle.data, + wallet_address=form.wallet_address.data, + ) + user.save() + login_user(user) + return redirect(url_for('main.index')) + + return render_template("auth/register.html", form=form) + +@bp.route("/login", methods=["GET", "POST"]) +def login(): + form = UserLogin() + if current_user.is_authenticated: + flash('Already logged in.') + return redirect(url_for('main.index')) + + if form.validate_on_submit(): + # Check if user doesn't exist + user = User.select().where( + User.handle == form.handle.data + ).first() + if not user: + flash('That handle does not exist.') + return redirect(url_for('auth.login')) + return redirect(url_for('auth.challenge', handle=user.handle)) + + return render_template("auth/login.html", form=form) + +@bp.route("/login/challenge/", methods=["GET", "POST"]) +def challenge(handle): + form = UserChallenge() + user = User.select().where(User.handle == handle).first() + if not user: + flash('User does not exist.') + return redirect(url_for('main.index')) + + if current_user.is_authenticated: + 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) + flash('Successful login!') + return redirect(url_for('main.index')) + else: + flash('Invalid signature. Try again.') + return redirect(url_for('auth.challenge', handle=handle)) + except Exception as e: + flash(f'Issue with checking the signature provided: {e}') + return redirect(url_for('auth.challenge', handle=handle)) + + return render_template( + 'auth/challenge.html', + user=user, + form=form + ) + +@bp.route("/logout") +def logout(): + if current_user.is_authenticated: + logout_user() + else: + flash('Not authenticated!') + return redirect(url_for('main.index')) diff --git a/nerochan/routes/main.py b/nerochan/routes/main.py new file mode 100644 index 0000000..9f9835c --- /dev/null +++ b/nerochan/routes/main.py @@ -0,0 +1,30 @@ +from flask import Blueprint, render_template +from flask_login import current_user + +from nerochan.models import * + + +bp = Blueprint('main', 'main') + +@bp.route('/') +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 render_template( + 'index.html', + feed=feed + ) diff --git a/nerochan/routes/post.py b/nerochan/routes/post.py new file mode 100644 index 0000000..aa8fbe2 --- /dev/null +++ b/nerochan/routes/post.py @@ -0,0 +1,27 @@ +from flask import Blueprint, render_template, flash, redirect, url_for +from flask_login import current_user + +# from nerochan.models import Content, User + + +bp = Blueprint('post', 'post') + +# @bp.route('/post/') +# def show(post_id): +# post = TextPost.get_or_none(post_id) +# if post: +# if current_user.is_anonymous: +# flash('You must login to view this post.') +# return redirect(url_for('creator.show', username=post.creator.user.username)) +# user_subscriptions = Subscription.select().where( +# Subscription.active == True, +# Subscription.backer == current_user.backer_profile.first() +# ) +# if user_subscriptions: +# return render_template('post/show.html', post=post) +# else: +# flash('Viewing posts requires a subscription.') +# return redirect(url_for('creator.subscription', username=post.creator.user.username)) +# else: +# flash('That post does not exist.') +# return redirect(url_for('meta.index')) diff --git a/nerochan/static/css/main.css b/nerochan/static/css/main.css new file mode 100644 index 0000000..e69de29 diff --git a/nerochan/static/css/noty-relax.css b/nerochan/static/css/noty-relax.css new file mode 100644 index 0000000..f5f99ec --- /dev/null +++ b/nerochan/static/css/noty-relax.css @@ -0,0 +1,46 @@ +.noty_theme__relax.noty_bar { + margin: 4px 0; + overflow: hidden; + border-radius: 2px; + position: relative; } + .noty_theme__relax.noty_bar .noty_body { + padding: 10px; } + .noty_theme__relax.noty_bar .noty_buttons { + border-top: 1px solid #e7e7e7; + padding: 5px 10px; } + +.noty_theme__relax.noty_type__alert, +.noty_theme__relax.noty_type__notification { + background-color: #fff; + border: 1px solid #dedede; + color: #444; } + +.noty_theme__relax.noty_type__warning { + background-color: #FFEAA8; + border: 1px solid #FFC237; + color: #826200; } + .noty_theme__relax.noty_type__warning .noty_buttons { + border-color: #dfaa30; } + +.noty_theme__relax.noty_type__error { + background-color: #FF8181; + border: 1px solid #e25353; + color: #FFF; } + .noty_theme__relax.noty_type__error .noty_buttons { + border-color: darkred; } + +.noty_theme__relax.noty_type__info, +.noty_theme__relax.noty_type__information { + background-color: #78C5E7; + border: 1px solid #3badd6; + color: #FFF; } + .noty_theme__relax.noty_type__info .noty_buttons, + .noty_theme__relax.noty_type__information .noty_buttons { + border-color: #0B90C4; } + +.noty_theme__relax.noty_type__success { + background-color: #BCF5BC; + border: 1px solid #7cdd77; + color: darkgreen; } + .noty_theme__relax.noty_type__success .noty_buttons { + border-color: #50C24E; } diff --git a/nerochan/static/css/noty.css b/nerochan/static/css/noty.css new file mode 100644 index 0000000..ee33b8f --- /dev/null +++ b/nerochan/static/css/noty.css @@ -0,0 +1,222 @@ +.noty_layout_mixin, #noty_layout__top, #noty_layout__topLeft, #noty_layout__topCenter, #noty_layout__topRight, #noty_layout__bottom, #noty_layout__bottomLeft, #noty_layout__bottomCenter, #noty_layout__bottomRight, #noty_layout__center, #noty_layout__centerLeft, #noty_layout__centerRight { + position: fixed; + margin: 0; + padding: 0; + z-index: 9999999; + -webkit-transform: translateZ(0) scale(1, 1); + transform: translateZ(0) scale(1, 1); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-font-smoothing: subpixel-antialiased; + filter: blur(0); + -webkit-filter: blur(0); + max-width: 90%; } + +#noty_layout__top { + top: 0; + left: 5%; + width: 90%; } + +#noty_layout__topLeft { + top: 20px; + left: 20px; + width: 325px; } + +#noty_layout__topCenter { + top: 5%; + left: 50%; + width: 325px; + -webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +#noty_layout__topRight { + top: 20px; + right: 20px; + width: 325px; } + +#noty_layout__bottom { + bottom: 0; + left: 5%; + width: 90%; } + +#noty_layout__bottomLeft { + bottom: 20px; + left: 20px; + width: 325px; } + +#noty_layout__bottomCenter { + bottom: 5%; + left: 50%; + width: 325px; + -webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +#noty_layout__bottomRight { + bottom: 20px; + right: 20px; + width: 325px; } + +#noty_layout__center { + top: 50%; + left: 50%; + width: 325px; + -webkit-transform: translate(-webkit-calc(-50% - .5px), -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(calc(-50% - .5px), calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +#noty_layout__centerLeft { + top: 50%; + left: 20px; + width: 325px; + -webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +#noty_layout__centerRight { + top: 50%; + right: 20px; + width: 325px; + -webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1); + transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); } + +.noty_progressbar { + display: none; } + +.noty_has_timeout.noty_has_progressbar .noty_progressbar { + display: block; + position: absolute; + left: 0; + bottom: 0; + height: 3px; + width: 100%; + background-color: #646464; + opacity: 0.2; + filter: alpha(opacity=10); } + +.noty_bar { + -webkit-backface-visibility: hidden; + -webkit-transform: translate(0, 0) translateZ(0) scale(1, 1); + -ms-transform: translate(0, 0) scale(1, 1); + transform: translate(0, 0) scale(1, 1); + -webkit-font-smoothing: subpixel-antialiased; + overflow: hidden; } + +.noty_effects_open { + opacity: 0; + -webkit-transform: translate(50%); + -ms-transform: translate(50%); + transform: translate(50%); + -webkit-animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); + animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; } + +.noty_effects_close { + -webkit-animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); + animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; } + +.noty_fix_effects_height { + -webkit-animation: noty_anim_height 75ms ease-out; + animation: noty_anim_height 75ms ease-out; } + +.noty_close_with_click { + cursor: pointer; } + +.noty_close_button { + position: absolute; + top: 2px; + right: 2px; + font-weight: bold; + width: 20px; + height: 20px; + text-align: center; + line-height: 20px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 2px; + cursor: pointer; + -webkit-transition: all .2s ease-out; + transition: all .2s ease-out; } + +.noty_close_button:hover { + background-color: rgba(0, 0, 0, 0.1); } + +.noty_modal { + position: fixed; + width: 100%; + height: 100%; + background-color: #000; + z-index: 10000; + opacity: .3; + left: 0; + top: 0; } + +.noty_modal.noty_modal_open { + opacity: 0; + -webkit-animation: noty_modal_in .3s ease-out; + animation: noty_modal_in .3s ease-out; } + +.noty_modal.noty_modal_close { + -webkit-animation: noty_modal_out .3s ease-out; + animation: noty_modal_out .3s ease-out; + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; } + +@-webkit-keyframes noty_modal_in { + 100% { + opacity: .3; } } + +@keyframes noty_modal_in { + 100% { + opacity: .3; } } + +@-webkit-keyframes noty_modal_out { + 100% { + opacity: 0; } } + +@keyframes noty_modal_out { + 100% { + opacity: 0; } } + +@keyframes noty_modal_out { + 100% { + opacity: 0; } } + +@-webkit-keyframes noty_anim_in { + 100% { + -webkit-transform: translate(0); + transform: translate(0); + opacity: 1; } } + +@keyframes noty_anim_in { + 100% { + -webkit-transform: translate(0); + transform: translate(0); + opacity: 1; } } + +@-webkit-keyframes noty_anim_out { + 100% { + -webkit-transform: translate(50%); + transform: translate(50%); + opacity: 0; } } + +@keyframes noty_anim_out { + 100% { + -webkit-transform: translate(50%); + transform: translate(50%); + opacity: 0; } } + +@-webkit-keyframes noty_anim_height { + 100% { + height: 0; } } + +@keyframes noty_anim_height { + 100% { + height: 0; } } + +/*# sourceMappingURL=noty.css.map*/ + + +/* Custom */ +.noty_body { + text-align: center; +} diff --git a/nerochan/static/css/noty.css.map b/nerochan/static/css/noty.css.map new file mode 100644 index 0000000..70e0c46 --- /dev/null +++ b/nerochan/static/css/noty.css.map @@ -0,0 +1 @@ +{"version":3,"sources":[],"names":[],"mappings":"","file":"noty.css","sourceRoot":""} \ No newline at end of file diff --git a/nerochan/static/images/monero-logo.png b/nerochan/static/images/monero-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bf7ab3261b16e36f6ca9dbfa07290f3b2d4b5d5b GIT binary patch literal 11912 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 literal 0 HcmV?d00001 diff --git a/nerochan/templates/auth/challenge.html b/nerochan/templates/auth/challenge.html new file mode 100644 index 0000000..aa3468b --- /dev/null +++ b/nerochan/templates/auth/challenge.html @@ -0,0 +1,11 @@ +{% extends 'includes/base.html' %} + +{% block content %} + +

Challenge

+

Handle: {{ user.handle }}

+

Challenge: {{ user.challenge }}

+

Wallet Address: {{ user.wallet_address }}

+{% include 'includes/form.html' %} + +{% endblock %} diff --git a/nerochan/templates/auth/login.html b/nerochan/templates/auth/login.html new file mode 100644 index 0000000..a962ae5 --- /dev/null +++ b/nerochan/templates/auth/login.html @@ -0,0 +1,8 @@ +{% extends 'includes/base.html' %} + +{% block content %} + +

Login

+{% include 'includes/form.html' %} + +{% endblock %} diff --git a/nerochan/templates/auth/register.html b/nerochan/templates/auth/register.html new file mode 100644 index 0000000..f68216c --- /dev/null +++ b/nerochan/templates/auth/register.html @@ -0,0 +1,8 @@ +{% extends 'includes/base.html' %} + +{% block content %} + +

Register

+{% include 'includes/form.html' %} + +{% endblock %} diff --git a/nerochan/templates/includes/base.html b/nerochan/templates/includes/base.html new file mode 100644 index 0000000..85fa9c4 --- /dev/null +++ b/nerochan/templates/includes/base.html @@ -0,0 +1,18 @@ + + + {% include 'includes/head.html' %} + + {% include 'includes/header.html' %} + {% block content %}{% endblock %} + {% include 'includes/debug.html' %} + + + diff --git a/nerochan/templates/includes/debug.html b/nerochan/templates/includes/debug.html new file mode 100644 index 0000000..812cf15 --- /dev/null +++ b/nerochan/templates/includes/debug.html @@ -0,0 +1,9 @@ +{% if config.DEBUG and current_user.is_authenticated %} +
+

Debug

+

+ Username: {{ current_user.handle }}
+ {% if current_user.email %}Email: {{ current_user.email }}
{% endif %} + Wallet Address: {{ current_user.wallet_address }}
+

+{% endif %} diff --git a/nerochan/templates/includes/footer.html b/nerochan/templates/includes/footer.html new file mode 100644 index 0000000..bfaf3f5 --- /dev/null +++ b/nerochan/templates/includes/footer.html @@ -0,0 +1,18 @@ +
+ + + +
+
    +
+ +
diff --git a/nerochan/templates/includes/form.html b/nerochan/templates/includes/form.html new file mode 100644 index 0000000..98f3518 --- /dev/null +++ b/nerochan/templates/includes/form.html @@ -0,0 +1,18 @@ +
+ {% for f in form %} + {% if f.name == 'csrf_token' %} + {{ f }} + {% else %} +
+ {{ f.label }} + {{ f }} +
+ {% endif %} + {% endfor %} +
    + {% for field, errors in form.errors.items() %} +
  • {{ form[field].label }}: {{ ', '.join(errors) }}
  • + {% endfor %} +
+ +
diff --git a/nerochan/templates/includes/head.html b/nerochan/templates/includes/head.html new file mode 100644 index 0000000..e8068a8 --- /dev/null +++ b/nerochan/templates/includes/head.html @@ -0,0 +1,22 @@ + + {{ config.SITE_NAME }} + + + + + + + + + + + + + + + + + + + + diff --git a/nerochan/templates/includes/header.html b/nerochan/templates/includes/header.html new file mode 100644 index 0000000..7cacdbc --- /dev/null +++ b/nerochan/templates/includes/header.html @@ -0,0 +1,20 @@ + + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +

+ {% for category, message in messages %} +

{{ message }} - {{ category }}

+ {% endfor %} +

+ {% endif %} +{% endwith %} diff --git a/nerochan/templates/index.html b/nerochan/templates/index.html new file mode 100644 index 0000000..55f06cc --- /dev/null +++ b/nerochan/templates/index.html @@ -0,0 +1,9 @@ +{% extends 'includes/base.html' %} + +{% block content %} + +{% if current_user.is_authenticated %} +

logged in: {{ current_user.handle }}

+{% endif %} + +{% endblock %} diff --git a/nerochan/templates/post/show.html b/nerochan/templates/post/show.html new file mode 100644 index 0000000..80d20b4 --- /dev/null +++ b/nerochan/templates/post/show.html @@ -0,0 +1,25 @@ + + + + {% include 'includes/head.html' %} + + +
+ + {% include 'includes/header.html' %} + + {% if post %} +

{{ post.title }}

+

Posted: {{ post.post_date }}

+

Edited: {{ post.last_edit_date }}

+

{{ post.content }}

+ {% endif %} + + {% include 'includes/footer.html' %} + +
+ + {% include 'includes/scripts.html' %} + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2dcc89c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +Flask-WTF +flask-login +flask-bcrypt +hypercorn +Pillow +psycopg2-binary +python-dotenv +qrcode +redis +peewee +requests +WTForms +flask-session +flask +monero +arrow +flake8