Compare commits

..

1 Commits

Author SHA1 Message Date
lza_menace ff30728863 making graphs n shit
3 years ago

2
.gitignore vendored

@ -6,4 +6,4 @@ __pycache__
*tar.gz
*sql
flask_session
.env
config.py

@ -1,15 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

@ -0,0 +1,9 @@
setup:
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
dev:
./bin/dev
prod:
./bin/prod

@ -1,99 +1,40 @@
# SuchWow!
Yo. This is a goofy little CRUD app that interacts with wownero-wallet-rpc to allow people to post memes and get tipped in WOW. It uses [Wownero's SSO & Identity service](https://login.wownero.com/developer/docs/backend) as an authentication backend.
It's lacking a lot of advanced functionality and features....but is pretty rock solid in operation. I rarely have to interact w/ my VPS, it just goes...
It was created haphazardly and drunkenly (at varying times). It (hopefully) stays true to the core ethos of Wownero (I feel) which is, "fuck it, lgtm, ship it". Rough around the edges but overall works great. Was fun to whip together and a blast to see it grow with a community behind it! Thanks all!
## Design
Using the "account" mechanism in the wallets (not subaddresses). Every post gets it's own Wownero account within that running wallet RPC process and is mapped to it. Funds go into the account when tipped. The payout script just checks the balances of each account on an interval and sends funds to the user associated with the artwork associated with said account.
TBD
## Setup
There are quite a few prerequisites to run the web service:
* [register](https://login.wownero.com/developer/register) your app on [Wownero SSO](https://login.wownero.com/developer/docs/backend)
* install Wownero binaries/software
* setup secrets and config
* initialize new Wownero wallet and retain seed
* run the wownero-wallet-rpc process
* install local system Python dependencies
* initialize new sqlite db
* setup scheduled tasks to run payouts
I like to ship with Docker, adjust how you'd prefer.
```
# setup secrets in env file outside of git
cp env-example .env
# register on wownero sso
# https://login.wownero.com/developer/register
# inject generated keys into suchwow config
# create new secrets for new wallet, flask server, data, etc
vim .env
# install docker
sudo apt-get install docker.io docker-compose -y
usermod -aG docker ubuntu
sudo -u ubuntu bash # login w/ new group perms
# run wownero wallets via docker - store seed, ensure rpc running `docker ps`
./run_wallets.sh
# install python dependencies locally
sudo apt-get install python3-venv -y
# initialize new wallet and retain seed
docker run --rm -it --name suchwow-wallet-init \
-v $(pwd)/data:/root \
lalanza808/wownero \
wownero-wallet-cli \
--daemon-address https://node.suchwow.xyz:443 \
--generate-new-wallet /root/wow \
--password zzzzzz \
# setup rpc process
docker run --rm -d --name suchwow-wallet \
-v $(pwd)/data:/root \
-p 8888:8888 \
lalanza808/wownero \
wownero-wallet-rpc \
--daemon-address https://node.suchwow.xyz:443 \
--wallet-file /root/wow \
--password zzzzzz \
--rpc-bind-port 8888 \
--rpc-bind-ip 0.0.0.0 \
--confirm-external-bind \
--rpc-login xxxx:yyyy \
--log-file /root/rpc.log
# install python dependencies
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# initialize a new sqlite db
./manage.sh init
# setup recurring payouts
# see crontab.txt for cron syntax example
crontab -e
# run a dev server
./manage.sh run # access on localhost:5000 - flask local dev, not for prod
# run a prod server
./manage.sh prod # run gunicorn on loopback - pass to nginx reverse proxy
# kill prod server
pkill -e -f gunicorn
# setup secrets in config file outside of git
cp suchwow/config.example.py suchwow/config.py
vim !$
```
# Operational Tasks
You'll want to promote a user as a moderator to review posts:
```
./manage.sh add_admin lza_menace
```
or remove them:
```
./manage.sh remove_admin lza_menace
```
Wallets bug out fairly often. Sometimes you might need to punch in a new daemon or restart a wallet:
```
docker stop suchwow-wallet-rpc
docker rm suchwow-wallet-rpc
vim .env
# modify DAEMON_URI
./run_wallets.sh
```
Manually run a payout:
```
./manage.py payout_users
```
There's other shit, but this is the most common. You'll get the hang of it.

@ -0,0 +1,7 @@
#!/bin/bash
source .venv/bin/activate
export FLASK_APP=suchwow/app.py
export FLASK_SECRETS=config.py
export FLASK_DEBUG=1
flask $@

@ -0,0 +1,7 @@
#!/bin/bash
source .venv/bin/activate
export FLASK_APP=suchwow/app.py
export FLASK_SECRETS=config.py
export FLASK_DEBUG=1
flask run

@ -0,0 +1,20 @@
#!/bin/bash
BASE=data/gunicorn
source .venv/bin/activate
export FLASK_APP=suchwow/app.py
export FLASK_SECRETS=config.py
export FLASK_DEBUG=0
export FLASK_ENV=production
mkdir -p $BASE
gunicorn \
--bind 0.0.0.0:4000 "suchwow.app:app" \
--daemon \
--log-file $BASE/gunicorn.log \
--pid $BASE/gunicorn.pid \
--reload
echo "Starting gunicorn"

@ -3,26 +3,29 @@
set -e
set +x
# mainnet wownero
export $(cat .env)
# these are only used for local development
WALLET_PATH="$(pwd)/data/suchwow-wallet"
WALLET_PASS="sdfj209rFLJDF29ruafj2)__!a@"
WALLET_RPC_USER="suchwow"
WALLET_RPC_PASS="y8YzL3cIW6Yeifa23s7Yng=="
DAEMON_URI="http://node.suchwow.xyz:34568"
if [ ! -d "$WALLET_PATH" ]; then
# initialize new wallet and retain seed
docker run --rm -it --name suchwow-wallet-init \
-v $WALLET_PATH:/root \
lalanza808/wownero:latest \
lalanza808/wownero \
wownero-wallet-cli \
--daemon-address $DAEMON_URI \
--generate-new-wallet /root/wow \
--password $WALLET_PASS
fi
# run rpc process
docker run --restart=always -d --name suchwow-wallet-rpc \
# setup rpc process
docker run --rm -d --name suchwow-wallet-rpc \
-v $WALLET_PATH:/root \
-p 8888:8888 \
lalanza808/wownero:latest \
lalanza808/wownero \
wownero-wallet-rpc \
--daemon-address $DAEMON_URI \
--wallet-file /root/wow \

@ -1,25 +0,0 @@
OIDC_URL=https://login.wownero.com/auth/realms/master/protocol/openid-connect
OIDC_CLIENT_ID=suchwow-dev
OIDC_CLIENT_SECRET=yyy-yyyyy-yyyyy-yy
OIDC_REDIRECT_URL=http://localhost:5000/auth
SECRET_KEY=ssssssssssss
DATA_FOLDER=/absolute/path/to/the/place/you/store/images
SERVER_NAME=localhost:5000
WALLET_PATH=/absolute/path/to/the/place/you/store/wallet
WALLET_PASS=mytopsecretpass
WALLET_HOST=localhost
WALLET_PORT=8888
WALLET_PROTO=http
WALLET_RPC_USER=suchwow
WALLET_RPC_PASS=again
DAEMON_URI=http://node.suchwow.xyz:34568
PRAW_CLIENT_SECRET=xxxx
PRAW_CLIENT_ID=xxxxx
PRAW_USER_AGENT=xxxxx
PRAW_USERNAME=xxxxx
PRAW_PASSWORD=xxxx
DISCORD_URL=https://xxxx
MM_ICON=https://xxxx
MM_CHANNEL=xxxx
MM_USERNAME=xxxx
MM_ENDPOINT=https://xxxx

@ -1,29 +0,0 @@
#!/bin/bash
source .venv/bin/activate
export FLASK_APP=suchwow/app.py
export FLASK_SECRETS=config.py
export FLASK_DEBUG=0
export FLASK_ENV=production
# override
export $(cat .env)
if [[ ${1} == "prod" ]];
then
export BASE=./data/gunicorn
mkdir -p $BASE
pgrep -F $BASE/gunicorn.pid
if [[ $? != 0 ]]; then
gunicorn \
--bind 127.0.0.1:4000 "suchwow.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

@ -7,5 +7,3 @@ six
praw
qrcode
Pillow
arrow
python-dotenv

@ -1,7 +1,5 @@
import json
import click
import arrow
from math import ceil
from datetime import datetime, timedelta
from random import choice
from os import makedirs, path, remove
@ -12,11 +10,10 @@ from suchwow import config
from suchwow.models import Post, Profile, Comment, Notification, db, Moderator
from suchwow.routes import auth, comment, post, profile, leaderboard, api
from suchwow.utils.decorators import login_required, moderator_required
from suchwow.utils.helpers import post_webhook, get_latest_tipped_posts
from suchwow.utils.helpers import get_top_posters, get_top_posts
from suchwow.utils.helpers import post_webhook
from suchwow.reddit import make_post
from suchwow.discord import post_discord_webhook
from suchwow import wownero, filters
from suchwow import wownero
app = Flask(__name__)
@ -30,23 +27,12 @@ app.register_blueprint(profile.bp)
app.register_blueprint(comment.bp)
app.register_blueprint(leaderboard.bp)
app.register_blueprint(api.bp)
app.register_blueprint(filters.bp)
@app.route("/")
def index():
itp = 15
itp = 20
page = request.args.get("page", 1)
submitter = request.args.get("submitter", None)
content = request.args.get("content", None)
if content == 'latest_tipped':
posts = get_latest_tipped_posts()
return render_template(
"index.html",
posts=posts[0:30],
title="Latest Tipped Memes"
)
try:
page = int(page)
except:
@ -55,18 +41,10 @@ def index():
posts = Post.select().where(Post.approved==True).order_by(Post.timestamp.desc())
if submitter:
posts = posts.where(Post.submitter==submitter)
paginated_posts = posts.paginate(page, itp)
total_pages = ceil(posts.count() / itp)
return render_template(
"index.html",
posts=paginated_posts,
page=page,
total_pages=total_pages,
title="Latest Memes"
)
posts = posts.where(Post.submitter == submitter)
posts = posts.paginate(page, itp)
total_pages = Post.select().count() / itp
return render_template("index.html", posts=posts, page=page, total_pages=total_pages)
@app.route("/mod")
@moderator_required
@ -78,6 +56,22 @@ def mod_queue():
def about():
return render_template("about.html")
@app.route("/stats")
def stats():
wow_txes = list()
posts = Post.select()
wallet = wownero.Wallet()
for post in posts:
txes = wallet.transfers(post.account_index)
if 'in' in txes:
wow_txes.append(txes['in'])
return render_template("stats.html", wow_txes=json.dumps(wow_txes))
@app.errorhandler(404)
def not_found(error):
flash("nothing there, brah")
return redirect(url_for("index"))
@app.cli.command("init")
def init():
# create subdirs
@ -87,22 +81,6 @@ def init():
# init db
db.create_tables([Post, Profile, Comment, Notification, Moderator])
@app.cli.command("post_reddit")
@click.argument('last_hours')
def post_reddit(last_hours):
posts = Post.select().where(
Post.approved==True,
Post.to_reddit==False
).order_by(Post.timestamp.asc())
for p in posts:
if p.hours_elapsed() < int(last_hours):
if not p.to_reddit:
_p = make_post(p)
if _p:
p.to_reddit = True
p.save()
return
@app.cli.command("create_accounts")
def create_accounts():
wallet = wownero.Wallet()
@ -120,7 +98,7 @@ def payout_users():
submitter = Profile.get(username=post.submitter)
balances = wallet.balances(post.account_index)
url = url_for('post.read', id=post.id, _external=True)
if balances[1] > 0.1:
if balances[1] > 0:
print(f"Post #{post.id} has {balances[1]} funds unlocked and ready to send. Sweeping all funds to user's address ({submitter.address}).")
sweep = wallet.sweep_all(account=post.account_index, dest_address=submitter.address)
print(sweep)
@ -128,8 +106,7 @@ def payout_users():
amount = 0
for amt in sweep["amount_list"]:
amount += int(amt)
# post_webhook(f"Paid out :moneybag: {_aw(_fa(amount))} WOW to `{post.submitter}` for post [{post.id}]({url})")
# god damn you eimernase, why'd you ruin this for me? :P
post_webhook(f"Paid out :moneybag: {_aw(_fa(amount))} WOW to `{post.submitter}` for post [{post.id}]({url})")
@app.cli.command("add_admin")
@click.argument("username")
@ -153,28 +130,5 @@ def remove_admin(username):
else:
print("That moderator doesn't exist")
@app.cli.command("show")
@click.argument("post_id")
def post_id(post_id):
p = Post.filter(id=post_id).first()
if p:
print(p.show())
else:
print("That post doesn't exist")
@app.cli.command("load_cache")
def load_cache():
app.logger.info('loading top posters into cache')
get_top_posters()
app.logger.info('done')
app.logger.info('loading latest tipped into cache')
get_latest_tipped_posts()
app.logger.info('done')
for i in [1, 3, 7, 30]:
app.logger.info(f'loading top posts last {i} days into cache')
get_top_posts(i)
app.logger.info('done')
if __name__ == "__main__":
app.run()

@ -0,0 +1,30 @@
from os import getenv
OIDC_URL = 'https://login.wownero.com/auth/realms/master/protocol/openid-connect',
OIDC_CLIENT_ID = 'suchwowxxx',
OIDC_CLIENT_SECRET = 'xxxxxxxxxx',
OIDC_REDIRECT_URL = 'http://localhost:5000/auth'
SECRET_KEY = 'yyyyyyyyyyyyy',
SESSION_TYPE = 'filesystem'
DATA_FOLDER = '/path/to/the/uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024
WALLET_HOST = 'localhost'
WALLET_PORT = 8888
WALLET_PROTO = 'http'
WALLET_USER = 'suchwow'
WALLET_PASS = 'zzzzzzzzzzzzzzz'
PRAW_CLIENT_SECRET = 'xxxxxxxx'
PRAW_CLIENT_ID = 'xxxxxxxx'
PRAW_USER_AGENT = 'suchwow-yyyy-python'
PRAW_USERNAME = 'xxxxxxxx'
PRAW_PASSWORD = 'xxxxxxxx'
SERVER_NAME = 'localhost'
DISCORD_URL = 'xxxxxxx'
BANNED_USERS = {'username': 'reason for the ban'}
MM_ICON = getenv('MM_ICON', 'https://funding.wownero.com/static/wowdoge-a.jpg')
MM_CHANNEL = getenv('MM_CHANNEL', 'suchwow')
MM_USERNAME = getenv('MM_USERNAME', 'SuchWow!')
MM_ENDPOINT = getenv('MM_ENDPOINT', 'ppppppppppppppppppppppppp')

@ -1,46 +0,0 @@
from os import getenv
from dotenv import load_dotenv
load_dotenv()
# generated from https://login.wownero.com/developer/register
OIDC_URL = getenv('OIDC_URL', 'https://login.wownero.com/auth/realms/master/protocol/openid-connect')
OIDC_CLIENT_ID = getenv('OIDC_CLIENT_ID', 'suchwow-dev')
OIDC_CLIENT_SECRET = getenv('OIDC_CLIENT_SECRET', '')
OIDC_REDIRECT_URL = getenv('OIDC_REDIRECT_URL', 'http://localhost:5000/auth')
# you specify something
SECRET_KEY = getenv('SECRET_KEY', 'yyyyyyyyyyyyy') # whatever you want it to be
DATA_FOLDER = getenv('DATA_FOLDER', '/path/to/uploads') # some stable storage path
SERVER_NAME = getenv('SERVER_NAME', 'localhost') # name of your DNS resolvable site (.com)
WALLET_HOST = getenv('WALLET_HOST', 'localhost') #
WALLET_PORT = int(getenv('WALLET_PORT', 8888)) #
WALLET_PROTO = getenv('WALLET_PROTO', 'http') #
WALLET_RPC_USER = getenv('WALLET_RPC_USER', 'suchwow') #
WALLET_RPC_PASS = getenv('WALLET_RPC_PASS', 'suchwow') #
WALLET_PASS = getenv('WALLET_PASS', 'zzzzzzz') # You specify all these wallet details in .env
# Optional for banning users who post crazy shit (they do)
BANNED_USERS = {'username': 'reason for the ban'}
# Optional for posting to Reddit
PRAW_CLIENT_SECRET = getenv('PRAW_CLIENT_SECRET', None)
PRAW_CLIENT_ID = getenv('PRAW_CLIENT_ID', None)
PRAW_USER_AGENT = getenv('PRAW_USER_AGENT', None)
PRAW_USERNAME = getenv('PRAW_USERNAME', None)
PRAW_PASSWORD = getenv('PRAW_PASSWORD', None)
# Optional for posting to Discord
DISCORD_URL = getenv('DISCORD_URL', None)
# Optional for posting to Mattermost
MM_ICON = getenv('MM_ICON', 'https://funding.wownero.com/static/wowdoge-a.jpg')
MM_CHANNEL = getenv('MM_CHANNEL', 'suchwow')
MM_USERNAME = getenv('MM_USERNAME', 'SuchWow!')
MM_ENDPOINT = getenv('MM_ENDPOINT', 'ppppppppppppppppppppppppp')
# defaults
SESSION_TYPE = 'filesystem'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024

@ -1,15 +0,0 @@
from flask import Blueprint, current_app
from arrow import get as arrow_get
bp = Blueprint('filters', 'filters')
@bp.app_template_filter('shorten_address')
def shorten_address(a):
_p = a[0:4]
_s = a[-4:]
return f'{_p}...{_s}'
@bp.app_template_filter('humanize')
def humanize(d):
return arrow_get(d).humanize()

@ -2,11 +2,10 @@ from peewee import *
from os import path
from datetime import datetime
from PIL import Image
from suchwow import wownero
from suchwow import config
db = SqliteDatabase(f"{config.DATA_FOLDER}/sqlite.db")
db = SqliteDatabase(f"{config.DATA_FOLDER}/db/sqlite.db")
class Post(Model):
id = AutoField()
@ -46,23 +45,6 @@ class Post(Model):
s = path.splitext(self.image_name)
return s[0] + '.thumbnail' + s[1]
def get_received_wow(self):
try:
w = wownero.Wallet()
it = w.incoming_transfers(self.account_index)
if 'transfers' in it:
amounts = [amt['amount'] for amt in it['transfers'] if 'transfers' in it]
return wownero.as_wownero(wownero.from_atomic(sum(amounts)))
else:
return 0
except:
return '?'
def hours_elapsed(self):
now = datetime.utcnow()
diff = now - self.timestamp
return diff.total_seconds() / 60 / 60
def show(self):
return {
'id': self.id,
@ -82,8 +64,6 @@ class Post(Model):
'to_reddit': self.to_reddit,
'to_discord': self.to_discord,
'approved': self.approved,
'received_wow': self.get_received_wow(),
'hours_elapsed': self.hours_elapsed()
}
class Meta:

@ -13,7 +13,6 @@ class Reddit(object):
password=config.PRAW_PASSWORD
)
self.subreddit = "wownero"
self.meme_flair_id = "2527d518-a96c-11ea-ba87-0e32c68cff8f"
def post(self, title, url):
try:
@ -21,7 +20,6 @@ class Reddit(object):
title=title,
url=url,
resubmit=False,
flair_id=self.meme_flair_id
)
return submission
except:
@ -35,9 +33,6 @@ class Reddit(object):
return False
def make_post(post):
if post.to_reddit:
print(f"Already posted #{post.id} to Reddit")
return False
wallet = wownero.Wallet()
title = f"SuchWow #{post.id} - {post.title}"
url = url_for('post.uploaded_file', filename=post.image_name, _external=True)

@ -1,63 +1,30 @@
from flask import jsonify, Blueprint, url_for, request, abort
from suchwow.models import Post
from suchwow import wownero
from suchwow.utils.helpers import rw_cache, get_top_posters, get_top_posts
bp = Blueprint("api", "api")
@bp.route("/api/list")
def api_list():
limit = request.args.get('limit', 30)
offset = request.args.get('offset', 0)
# Hacky way to convert query str value to int
try:
limit = int(limit)
offset = int(offset)
except:
abort(500, "Bleep bleep")
if limit > 30:
limit = 30
all_posts = []
posts = Post.select().where(Post.approved == True).order_by(
Post.timestamp.desc()).limit(limit).offset(offset)
for post in posts:
wallet = wownero.Wallet()
if wallet.connected:
address = wallet.get_address(account=post.account_index)
else:
address = ''
payload = {
'image': url_for('post.uploaded_file', filename=post.image_name, _external=True),
'submitter': post.submitter,
'address': address,
'title': post.title,
'text': post.text,
'href': url_for('post.read', id=post.id, _external=True),
'id': post.id,
'timestamp': post.timestamp
}
all_posts.append(payload)
return jsonify(all_posts)
@bp.route("/api/posters")
def api_posters():
posters = get_top_posters()
all_posters = {}
for poster, data in posters.items():
payload = {
"submitter": poster,
"posts": data["posts"],
"amount": data["amount"],
}
all_posters.append(payload)
return jsonify(posters)
from flask import jsonify, Blueprint, url_for
from suchwow.models import Post
from suchwow import wownero
bp = Blueprint("api", "api")
@bp.route("/api/list")
def api_list():
all_posts = []
posts = Post.select().where(Post.approved==True)
for post in posts:
wallet = wownero.Wallet()
if wallet.connected:
address = wallet.get_address(account=post.account_index)
else:
address = ''
payload = {
'image': url_for('post.uploaded_file', filename=post.image_name, _external=True),
'submitter': post.submitter,
'address': address,
'title': post.title,
'text': post.text,
'href': url_for('post.read', id=post.id, _external=True),
'id': post.id,
'timestamp': post.timestamp
}
all_posts.append(payload)
return jsonify(all_posts)

@ -1,30 +1,28 @@
from datetime import datetime, timedelta
from os import path
from flask import render_template, Blueprint, request, session, flash
from flask import send_from_directory, redirect, url_for, current_app
from werkzeug.utils import secure_filename
from suchwow import wownero
from suchwow.models import Post
from suchwow.utils.helpers import rw_cache, get_top_posters, get_top_posts
bp = Blueprint("leaderboard", "leaderboard")
@bp.route("/leaderboards/top_posters")
def top_posters():
top_posters = get_top_posters()
return render_template("leaderboard.html", posters=top_posters)
@bp.route("/leaderboards/top_posts")
def top_posts():
days = request.args.get('days', 1)
try:
days = int(days)
except:
days = 1
@bp.route("/leaderboard")
def leaderboard():
top_posters = {}
posts = Post.select().where(Post.approved==True)
for post in posts:
transfers = []
incoming = wownero.Wallet().incoming_transfers(post.account_index)
if "transfers" in incoming:
for xfer in incoming["transfers"]:
transfers.append(wownero.from_atomic(xfer["amount"]))
total = sum(transfers)
if post.submitter not in top_posters:
top_posters[post.submitter] = {"amount": 0, "posts": []}
if days not in [1, 3, 7, 30]:
days = 7
top_posters[post.submitter]["amount"] += float(total)
top_posters[post.submitter]["posts"].append(post)
posts = get_top_posts(days)
return render_template("post/top.html", posts=posts, days=days)
return render_template("leaderboard.html", posters=top_posters)

@ -1,4 +1,3 @@
from datetime import datetime, timedelta
from os import path, remove
from io import BytesIO
from base64 import b64encode
@ -11,14 +10,28 @@ from suchwow import wownero
from suchwow import config
from suchwow.models import Post, Comment
from suchwow.utils.decorators import login_required, profile_required, moderator_required
from suchwow.utils.helpers import allowed_file, is_moderator, get_session_user
from suchwow.utils.helpers import rw_cache, post_webhook
from suchwow.utils.helpers import allowed_file, is_moderator, get_session_user, post_webhook
from suchwow.reddit import make_post
from suchwow.discord import post_discord_webhook
bp = Blueprint("post", "post")
@bp.route("/posts/top")
def top():
top_posts = {}
posts = Post.select().where(Post.approved==True)
for post in posts:
transfers = []
incoming = wownero.Wallet().incoming_transfers(post.account_index)
if "transfers" in incoming:
for xfer in incoming["transfers"]:
transfers.append(wownero.from_atomic(xfer["amount"]))
total = sum(transfers)
if total > 0:
top_posts[float(total)] = post
return render_template("post/top.html", posts=sorted(top_posts.items(), reverse=True))
@bp.route("/post/<id>")
def read(id):
_address_qr = BytesIO()
@ -86,8 +99,7 @@ def create():
wallet = wownero.Wallet()
account_index = wallet.new_account()
except:
flash("Suchwow wallet is fucked up! Try again later.")
return redirect(request.url)
account_index = 0
post = Post(
title=post_title,
text=request.form.get("text", ""),
@ -116,18 +128,14 @@ def approve(id):
post_webhook(f"Post [{post.id}]({url}) approved :white_check_mark: by `{get_session_user()}` :fieri_parrot:")
flash("Approved")
if current_app.config["DEBUG"] is False:
# _r = None
# _d = None
# if not post.to_reddit:
# _r = make_post(post)
# if not post.to_discord:
# _d = post_discord_webhook(post)
# if _r and _d:
# post_webhook(f"Post [{post.id}]({url}) submitted :dab_parrot: to Reddit and Discord.")
# else:
# post_webhook(f"Post [{post.id}]({url}) failed :this-is-fine-fire: to post to socials...Reddit: {_r} - Discord: {_d}")
post_discord_webhook(post)
post_webhook(f"Post [{post.id}]({url}) submitted :dab_parrot: to Discord.")
if not post.to_reddit:
_r = make_post(post)
if not post.to_discord:
_d = post_discord_webhook(post)
if _r and _d:
post_webhook(f"Post [{post.id}]({url}) submitted :dab_parrot: to Reddit and Discord.")
else:
post_webhook(f"Post [{post.id}]({url}) failed :this-is-fine-fire: to post to socials...Reddit: {_r} - Discord: {_d}")
return redirect(url_for("mod_queue"))
else:
flash("You can't approve this")

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,342 @@
/*!
* Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com)
* Copyright 2011-2017 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: none !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
a,
area,
button,
[role="button"],
input:not([type="range"]),
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #868e96;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com)
* Copyright 2011-2017 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,9 +0,0 @@
body {
background-image: url("/static/bg.webp");
background-repeat: repeat;
}
.current-page-btn {
color: blue;
border: 1px blue solid;
}

@ -0,0 +1,793 @@
body {
padding-top: 54px;
}
@media (min-width: 992px) {
body {
padding-top: 56px;
}
}
.center {
text-align: center;
margin: 0 auto 0 auto;
}
.navbar {
padding: .1rem 1rem;
}
.navbar .navbar-brand img#logo{
width: 32px;
height: 32px;
display: inline-block;
vertical-align: middle;
}
.navbar .navbar-brand img#text{
width:235px;
margin-top: -4px;
padding-top: 0px;
display: inline-block;
}
.navbar-nav .nav-link{
color: #fff;
font-weight: bold;
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif
}
.container>div:first-child {
padding-top: 30px;
}
.mb-4, .my-4{
margin-top:0 !important;
}
.mt-4, .my-4 {
margin-top: 0rem !important;
}
.proposal-info-table td {
padding: .35rem !important;
}
.proposal_address {
white-space: pre-wrap;
padding: 2px;
border-radius: 2px;
margin-top: 6px;
font-weight: bold;
color: #ff0000;
font-size: 18px;
margin-bottom: 0;
}
.proposal_content blockquote {
background-color: #efefef;
padding: 8px;
}
.proposal_content h1, .proposal_content h2, .proposal_content h3, .proposal_content h4{
margin-bottom: 16px;
margin-top: 16px;
}
.proposal_content p {
margin-bottom: 0.5rem;
}
.proposal_content img {
margin-top:8px;
margin-bottom:8px;
max-width: 70%;
}
.proposal_content code {
background: #ebebeb;
padding: 2px;
}
.table td, .table th {
padding: 0.35rem;
}
/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px; /* Margin bottom by footer height */
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
background-image: url("/static/bg.png");
background-repeat: repeat;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 30px;
line-height: 30px;
background-color: #f5f5f5;
}
.bg-dark {
background: linear-gradient(90deg, #d253c8, #ffa93e);
}
.navbar-dark .navbar-brand {
color: #fff;
font-size: 18pt;
margin-top: 0;
padding-top: 0;
margin-bottom: 0;
padding-bottom: 0;
text-shadow: 2px 2px #667ff952;
font-family: "Comic Sans MS", "Comic Sans", cursive;
}
.table-proposal td {
font-size: 15px;
}
.table-proposal tbody td:first-child{
word-break: break-word;
width: 40%;
}
.table-hover tbody tr:hover {
background-color: rgba(0,0,0,.055);
}
.table th, .table td {
border-top: 1px solid #00000029;
padding-top: .65rem;
}
.table-tilted{
-ms-transform: rotate(0.5deg);
-webkit-transform: rotate(0.5deg);
transform: rotate(0.5deg);
}
.table-tilted-v{
-ms-transform: rotate(-0.2deg);
-webkit-transform: rotate(-0.2deg);
transform: rotate(-0.2deg);
}
.table-no-header thead{
display: none;
}
.table-wow{
background-image: url(https://wownero.win/static/game-table-bg.png);
background-size: cover;
}
.table a{
color: #006400;
}
.table thead th {
border-bottom: 1px solid #00000029 !important;
border-top: none;
font-size: 14px;
}
.table tbody td.date {
max-width: 100px;
}
.table tbody td.user {
font-size: 14px;
}
.table tbody td{
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active {
color: #495057;
background-color: #fff;
border-left-color: #008926;
border-left-width: 2px;
border-top-color: #008926;
border-top-width: 1px;
border-right-color: #008926;
border-right-width: 1px;
}
.nav-tabs .nav-link {
border: 1px solid transparent;
border-top-left-radius: .25rem;
border-top-right-radius: .25rem;
border-bottom-color: #008926;
}
.nav-tabs .nav-link:focus, .nav-tabs .nav-link:hover {
border-color: #e9ecef #e9ecef #dee2e6;
}
.nav-tabs .nav-link:focus, .nav-tabs .nav-link:hover {
border-color: #00892630 #00892630 #008926;
}
a {
color: #008926;
}
.btn-group-toggle a{
color: white;
}
.btn-group-toggle a:hover{
text-decoration: none;
}
.btn-primary{
background-color: #28a745;
border-color: #28a745;
}
.btn-primary:hover{
color: #fff;
background-color: #1e7e34;
border-color: #1c7430;
}
.card-body {
background-color: #00000008;
}
.card {
background-color: #ffffff00;
border: 0px solid rgba(0,0,0,.125);
border-radius: .25rem;
}
.card-header {
padding: 0.5rem;
padding-left: 1.0rem;
font-size: 16px;
margin-bottom: 0;
background-color: rgba(0,0,0,.03);
background: linear-gradient(90deg, #d253c829, #ffa93e69);
/*background: linear-gradient(90deg, #40d61e1f, #ffa93e69);*/
border-radius: calc(.25rem - 1px) calc(.25rem - 1px) 0 0;
border-bottom: 1px solid #e6e6e6;
border-bottom: 0px;
}
.proposal_content{
background-color: #00000008;
padding: 1.25rem 1.25rem;
}
.form-control {
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #008926;
}
.form-group {
margin-bottom: 1rem;
}
.form-group:last-of-type {
margin-bottom:2.5rem;
}
/*fuku chrome*/
input {
outline:none;
}
input:focus {
outline: 0;
}
*:focus {
outline: none;
}
input[type="text"], textarea {
outline: none;
box-shadow:none !important;
border: 1px solid #008926 !important;
}
.navbar-dark .navbar-nav .nav-link {
color: white;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
font-size: 15px;
padding-right: 1em;
}
.navbar-nav .nav-item {
line-height: 20px;
}
nav .nav-link{
color: white;
}
nav .nav-link .active{
text-shadow: none;
}
.proposal-info-table td{
border-top: 0;
}
.proposal-info-table td span.badge{
font-size:20px;
}
.table-proposal .progress {
max-width: 70px !important;
min-width: 50px !important;
float:right;
}
.table-proposal .progress-bar {
display: -ms-flexbox;
display: flex;
-ms-flex-pack: center;
justify-content: center;
color: #fff;
text-shadow: 1px 1px #000000;
text-align: center;
white-space: nowrap;
background-color: #009845;
transition: width .6s ease;
font-family: monospace;
padding: 6px;
}
#point-wow-left{
display: inline;
}
@media only screen and (max-width: 600px) {
.proposal-info-table tr:first-child>td, .proposal-info-table tr:first-child>td>span {
font-size: 28px !important;
font-weight: 100;
}
.navbar-dark .navbar-brand {
font-size: 13pt;
}
.proposal-info-table tr:first-child>td, .proposal-info-table tr:first-child>td>span {
font-size: 14px !important;
}
.proposal-info-table td span.badge {
font-size: 14px;
}
.proposal_content {
background-color: #00000008;
padding: 0.1rem 0.75rem;
}
.card-body{
padding: 0.5rem;
}
#point-wow-left{
display: none;
}
.table-proposal thead th#date, .table-proposal tbody td#date{
display: none;
}
.table-proposal, .table-proposal td{
font-size:14px !important;
}
.navbar-brand {
display: none;
}
.navbar-brand-mobile{
display: block !important;
color: white;
font-size:14px;
}
pre.proposal_address{
font-size:14px;
}
label {
font-size:12px !important;
}
}
@media only screen and (max-width: 768px) {
#point-wow-left {
display: none;
}
}
textarea.comment{
padding-left: 6px;
padding-right: 6px;
padding-bottom: 6px;
width: 100%;
max-width: 600px;
}
.votearrow {
width: 10px;
height: 10px;
border: 0px;
margin: 3px 2px 6px;
margin-right: 10px;
margin-top: 7px;
background: url(/static/grayarrow.gif) no-repeat;
}
span.username a{
font-family: Verdana, Geneva, sans-serif;
font-size: 12pt;
color: #828282;
}
.comment-container a.reply{
margin-bottom:2px;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
font-size: 11pt;
text-decoration: underline;
color: black;
}
span.date_posted a{
color: #828282;
font-family: Verdana, Geneva, sans-serif;
font-size: 10pt;
margin-left:2px;
}
.comment-container .comment-container{
margin-top: 0.4rem !important;
}
.comment-container, .comment-container .comment-container{
margin-bottom: 0.4rem !important;
font-family: serif;
font-size: 16px;
}
span.username a.author{
color: #008926;
font-weight: bold;
}
:target {
background: linear-gradient(90deg, #ff606008, #ffa93e2b);
}
.form-admin{
border: 1px solid #b60000;
}
ul.b {
list-style-type: square;
padding-left: 1.0rem;
}
.media-body #comment{
word-break: break-all;
}
.api_documentation code{
background-color: #f3f3f3;
padding: 4px;
padding-top:2px;
padding-bottom:2px;
}
.api_documentation pre{
margin-top:0;
background-color: #f3f3f3;
padding: 4px;
font-size: 12px;
padding-left:8px;
white-space: pre-wrap; /* Since CSS 2.1 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
.api_documentation .api_container hr{
margin-bottom: 14px;
margin-top: 12px;
}
.api_documentation .api_container{
margin-bottom: 60px;
}
.sidebar .card-body{
padding-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
font-size: 14px;
}
.sidebar .card-body ul.b{
margin-bottom:0;
}
.card-header {
border-radius: 10px 0 0 !important;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto-1.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto-2.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto-3.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto-4.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto-5.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto-6.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto-7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
.proposal-overview .table-proposal[data-status="4"] {
opacity: 0.4;
}
.proposal-overview .table-proposal[data-status="4"] thead th {
border-bottom: 1px solid #4b4b4b !important;
}
.proposal-overview .table-proposal[data-status="4"] td {
border-top: 1px solid #4b4b4b;
}
.proposal-overview .table-proposal[data-status="4"], .proposal-overview .table-proposal[data-status="4"] a {
color: #4b4b4b;
}
.comments-panel {
background-color: #ffffffb0;
}
.wow_addy{
color: red;
cursor: pointer;
font-family: monospace;
background: #f6f6f6;
font-size:14px;
}
.wow_addy[data-active="true"]{
cursor: default;
}
.proposal_qr{
margin-top:8px;
margin-bottom:8px;
}
.table-proposal tr {
background: #00000005;
}
.tx_item {
padding-top: 4px;
padding-bottom: 4px;
background: #ffffff80;
}
.tx_item .amount {
float:right;
font-weight:bold;
color:#890000;
}
.tx_item .amount.in {
color:#008926;
}
.tx_item .datetime {
font-size: 14px;
color: #999;
}
.tx_item .height b {
font-size:14px;
}
.container>.content h1,
.container>.content h2,
.container>.content h3,
.container>.content h4{
margin-bottom:20px;
}
.comment-container .media-body span.body {
word-wrap: break-word;
display: block;
margin-top: 4px;
margin-bottom: 4px;
}
.comment-container .media-body span.body img {
width: 100%;
max-width:500px;
max-height:600px;
margin-top:10px;
margin-bottom:10px;
display: block;
}
.navbar-brand {
margin-top:0
margin-bottom:0
padding-top:0
padding-bottom:0;
}
.card h5{
color: #0000009e;
font-size: 14px;
}
.wowtable{
width:100%;
display: block;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace
}
.wowtable .trow{
width: 100%;
height: 34px;
display: table;
}
.wowtable .trow .item{
float:right;
width:100px;
}
.wowtable .trow .item.one{
width:30%;
min-width: 200px;
}
.wowtable .trow .item.small{
max-width:100px;
text-align:right;
}
.wowtable .trow.header .item span {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
font-weight: 700;
padding-top:6px;
display: block;
}
.wowtable .trow .item.comments{
max-width:70px;
text-align: center;
}
.wowtable .trow .item.big{
display: inline-block;
text-align: left;
float: left;
width: auto;
max-width: 50%;
}
.wowtable .trow.header .item.big span{
padding-left:7px;
padding-top:3px;
font-size:18px;
word-break: break-word;
}
.trow.divider{
height:1px;
background: #00000029 !important;
}
.wowtable .trow.body .item span {
font-size:14px;
padding-top:6px;
display: block;
}
.wowtable .trow.body .item.big a {
display: block;
padding-left: 7px;
color: #006400;
padding-top: 6px;
font-size: 15px;
font-weight: 700;
word-break: break-word;
}
.wowtable .trow span.alignl{
text-align:left;
padding-left:7px;
}
@media (max-width: 990px) {
.wowtable .small{
display:none
}
.wowtable .trow .item.big{
max-width: 88%;
}
}
.subtext {
color: grey;
margin: 0;
}
.subtitle {
font-size: 1.2em;
}
.container {
margin: 0 auto;
display: block;
}
.flashes {
text-align: center;
font-size: 1.5em;
}
.navbar-nav {
flex-direction: row;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

@ -3,20 +3,17 @@
{% block content %}
<div class="container" style="text-align:center;">
<div class="about content">
<div class="about">
<h1>About</h1>
<p>Post memes! Have fun! Comment and talk shit! Earn WOW!</p>
<p>Quit your day job and become a full-time shitposter and memer!</p>
<p>Commiserate with the gang in IRC, get high, and let your responsibilities fall to the wayside.</p>
<p>Join the revolution!</p>
<br />
<br />
<p>If you've made WOW from memes, please consider donating to our mod team - we work hard to ensure only the finest quality memes are shown for your viewing pleasure:</p>
<p><strong>qvqc (top mod)</strong>: WW3rVRuwdB5Bb95mZuGpunguz7Ers3Gao6EGLYZ6z2uEA9RsYzbZN1rfT74gzEDxyfNSVcpnMvnUhZTLfjiE73eu29ErXTDi4</p>
<p><strong>lza_menace (dev)</strong>: WW3CRUnpWnAQmXyr8rgd5qVneg3tTKbRrZu2qbTzjwMNEtvddodV2inPbRSGjcdRyHKVNsNkwWccjN6iKu1FAGr32hqKzikQP</p>
<br />
<br />
<br />
<h1>Contact</h1>
<p>I run this site for fun because I like Wownero and enjoy messing around with it. It doesn't make me any money so do not expect quick replies about it. Here are places you can find me:</p>
<ul>
<li><a href="https://webchat.freenode.net/?room=#wownero">IRC</a></li>
<li><a href="https://twitter.com/lza_menace">Twitter</a></li>
<li><a href="mailto:lza_menace@protonmail.com">Email</a></li>
<li><a href="https://www.reddit.com/user/lza_menace">Reddit</a></li>
</ul>
</div>
</div>

@ -20,7 +20,7 @@
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<link rel="shortcut icon" href="/static/wownero_logo.ico" type="image/x-icon" />
<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon" />
{% if post %}
<meta property="og:url" content="{{ url_for('post.read', id=post.id, _external=True) }}" />
<meta property="og:image" content="{{ url_for('post.uploaded_file', filename=post.image_name, _external=True) }}" />
@ -46,66 +46,37 @@
<meta name="application-name" content="SuchWow!">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="keywords" content="wownero, wow, memes, monero, xmr, cryptocurrency">
<link href="/static/css/bulma.min.css" rel="stylesheet">
<link href="/static/css/custom.css" rel="stylesheet">
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/bootstrap-grid.min.css" rel="stylesheet">
<link href="/static/css/bootstrap-reboot.min.css" rel="stylesheet">
<link href="/static/css/wow.css" rel="stylesheet">
<title>SuchWow!</title>
</head>
<body>
{% include 'navbar.html' %}
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<div class="notification is-danger container is-light">
{{ message }}
</div>
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% include 'navbar.html' %}
<!-- Page Content -->
{% block content %} {% endblock %}
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>SuchWow</strong> by <a href="https://lzahq.tech" target="_blank">lza_menace</a></p>
<p><a href="https://wownero.org/" target="_blank">Learn more about the infamous shitcoin, Wownero</a>.</p>
</div>
</footer>
<!-- JS -->
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Check if there are any navbar burgers
if ($navbarBurgers.length > 0) {
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
}
});
</script>
<footer class="bg-dark footer">
<div class="container">
<p class="m-0 text-center text-white">SuchWow! 2020</p>
</div>
</footer>
</body>
</html>

@ -4,64 +4,52 @@
<div class="container" style="text-align:center;">
<h1 class="title">{% if title %}{{ title }}{% else %}Latest Memes{% endif %}</h1>
<h3>Leaderboards</h3>
<a href="{{ url_for('post.top') }}"><button class="btn btn-warning">Top Posts</button></a>
<a href="{{ url_for('leaderboard.leaderboard') }}"><button class="btn btn-warning">Top Posters</button></a>
<hr>
{% if request.args.content == 'latest_tipped' %}
<a href="/">View Latest Memes</a>
{% else %}
<a href="/?content=latest_tipped">View Latest Tipped Posts</a>
{% endif %}
<div class="title">
<h3>{% block title %}Latest Posts{% endblock %}</h3>
</div>
<section class="section">
{% if posts %}
{% for row in posts | batch(4) %}
<div class="columns">
{% for post in row %}
{% set post = post.show() %}
<div class="column">
<div class="card">
<div class="card-image">
<a href="{{ url_for('post.read', id=post.id) }}">
<img src="{{ url_for('post.uploaded_file', filename=post.thumbnail_name) }}" alt="Placeholder image">
</a>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<p class="title is-4">
<a href="{{ url_for('post.read', id=post.id) }}">{{ post.title }}</a>
</p>
<p class="subtitle is-6"><a href="/?submitter={{ post.submitter }}">{{ post.submitter }}</a></p>
</div>
</div>
<div class="content">
{{ post.text | truncate(60) }}
<p><strong>{{ post.received_wow }} WOW received</strong></p>
<time datetime="2016-1-1">{{ post.timestamp.year }}-{{ post.timestamp.month }}-{{ post.timestamp.day }} {{ post.timestamp.hour }}:{{ post.timestamp.minute }} UTC</time>
<p>({{ post.timestamp | humanize }})</p>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<table class="table table-striped">
<tr>
<th>Date</th>
<th>ID</th>
<th>Title</th>
<th>Thumbnail</th>
<th>Submitter</th>
</tr>
{% for post in posts %}
<tr>
<td>{{ post.timestamp.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ post.id }}</td>
<td><a href="{{ url_for('post.read', id=post.id) }}">{{ post.title }}</a></td>
<td>
<a href="{{ url_for('post.read', id=post.id) }}">
<img src="{{ url_for('post.uploaded_file', filename=post.get_thumbnail_name()) }}" width=200 style="max-height:200px;">
</a>
</td>
<td><a href="/?submitter={{ post.submitter }}">{{ post.submitter }}</a></td>
</tr>
{% endfor %}
</table>
{% else %}
<p>No posts yet!</p>
{% endif %}
</section>
{% if total_pages %}
<nav class="pagination is-centered pb-4" role="navigation" aria-label="pagination">
<ul class="pagination-list">
{% for p in range(1, total_pages + 1) %}
<a href="{% if request.args.submitter %}/?submitter={{ request.args.submitter }}&{% else %}/?{% endif %}page={{ p }}" class="pagination-link {% if p == page %}current-page-btn{% endif %}">{{ p }}</a>
{% endfor %}
</ul>
</nav>
{% if page %}
{% if page > 1 %}
<a href="{% if request.args.submitter %}/?submitter={{ request.args.submitter }}&{% else %}/?{% endif %}page={{ page - 1 }}" style="padding:1em;">Back</a>
{% endif %}
{% if page < total_pages and total_pages > 0 %}
<a href="{% if request.args.submitter %}/?submitter={{ request.args.submitter }}&{% else %}/?{% endif %}page={{ page + 1 }}" style="padding:1em;">Next</a>
{% endif %}
{% endif %}
</div>

@ -2,9 +2,11 @@
{% block content %}
<div class="container content" style="text-align:center;">
<div class="container" style="text-align:center;">
<h1>Top Posters</h1>
<div class="title">
<h3>{% block title %}Top Posters{% endblock %}</h3>
</div>
{% if posters %}
<table class="table table-striped">

@ -1,49 +1,44 @@
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<img src="/static/wow_logo.webp">
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{{ url_for('index') }}">Home</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">Leaderboards</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('leaderboard.top_posters') }}">Top Posters</a>
<a class="navbar-item" href="{{ url_for('leaderboard.top_posts') }}?days=1">Top Memes Last 1 Day</a>
<a class="navbar-item" href="{{ url_for('leaderboard.top_posts') }}?days=3">Top Memes Last 3 Days</a>
<a class="navbar-item" href="{{ url_for('leaderboard.top_posts') }}?days=7">Top Memes Last Week</a>
<a class="navbar-item" href="{{ url_for('leaderboard.top_posts') }}?days=30">Top Memes Last Month</a>
</div>
</div>
<a class="navbar-item" href="{{ url_for('about') }}">About</a>
<a class="navbar-item" href="{{ url_for('post.create') }}">Submit</a>
{% if session.auth %}
<a class="navbar-item" style="color: orange;" href="{{ url_for('mod_queue') }}">Mods!</a>
{% endif %}
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
{% if not 'auth' in session %}
<a class="button is-primary" href="https://login.wownero.com/developer/"><strong>Sign up</strong></a>
<a class="button is-light" href="{{ url_for('auth.login') }}">Log in</a>
{% else %}
<a class="button is-primary" href="{{ url_for('profile.edit') }}">Profile ({{ session.auth.preferred_username }})</a>
<a class="button is-light" href="{{ url_for('auth.logout') }}">Logout</a>
{% endif %}
</div>
</div>
</div>
</div>
</nav>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="/">
<h3>SuchWow!</h3>
</a>
<div class="" id="">
<ul class="navbar-nav ml-auto">
{% if session.auth %}
<li class="nav-item">
<a class="nav-link" style="color: yellow;" href="{{ url_for('mod_queue') }}">Mods!</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('index') }}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('post.create') }}">Submit</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('about') }}">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('stats') }}">Stats</a>
</li>
{% if session.auth == None %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
</li>
{% else %}
<li class="nav-item">
{% if session.auth %}
<a class="nav-link" href="{{ url_for('profile.edit') }}">Profile ({{ session.auth.preferred_username }})</a>
{% endif %}
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>

@ -3,15 +3,8 @@
{% block content %}
<div class="container" style="width:40%;">
<div class="submit content">
<div class="submit">
<h1>Submit A Meme</h1>
<h2>Rules</h2>
<ol>
<li>no super low effort memes</li>
<li>no nsfl and nsfw</li>
<li>must pertain to wownero or crypto in general</li>
</ol>
<form method=post enctype=multipart/form-data class="form-horizontal">
<div class="form-group">
<label class="sr-only" for="inlineFormInput">Title</label>

@ -3,78 +3,51 @@
{% block content %}
<div class="container">
<div class="post" style="text-align:center;">
<nav class="breadcrumb is-centered" aria-label="breadcrumbs">
<ul>
<li><a href="/">All Posts</a></li>
<li class="is-active"><a href="#" aria-current="page">Post {{ post.id }}</a></li>
</ul>
</nav>
<a href="/">< Go Back</a>
{% if post.hidden %}
<h2>You cannot see this post</h2>
{% else %}
<!-- Post Info -->
<section class="section">
<div class="content">
<h1>{{ post.title }}</h1>
<p>{{ post.text }}</p>
{% if not post.approved %}
<a href="{{ url_for('post.approve', id=post.id) }}"><button type="button" name="button">Approve</button></a>
<a href="{{ url_for('post.delete', id=post.id) }}"><button type="button" name="button">Reject</button></a>
{% endif %}
<p>Submitted by <i><u><a href="/?submitter={{ post.submitter }}">{{ post.submitter }}</a></u></i> at <i>{{ post.timestamp }}</i></p>
<img src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" width=600/ style="border-radius:4px;">
</div>
</section>
<h1>{{ post.title }}</h1>
<p class="subtitle">{{ post.text }}</p>
{% if not post.approved %}
<a href="{{ url_for('post.approve', id=post.id) }}"><button type="button" name="button">Approve</button></a>
<a href="{{ url_for('post.delete', id=post.id) }}"><button type="button" name="button">Reject</button></a>
{% endif %}
<p class="subtext">Submitted by <i><u><a href="/?submitter={{ post.submitter }}">{{ post.submitter }}</a></u></i> at <i>{{ post.timestamp }}</i></p>
<br>
<img src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" width=600/ style="margin-bottom:1em;border-radius:4px;">
<hr>
<!-- Payments -->
<section class="section content">
<h3>Payments</h3>
<p style="word-break:break-all;">Vote for this post by sending WOW to the following address:<br><i>{{ address }}</i></p>
{% if qr_code %}
<img src="data:image/png;base64,{{ qr_code }}" width=180 class="center"><br /><br />
{% endif %}
<div class="columns">
<div class="column content">
<h4>WOW Received</h4>
{% if transfers.in %}
<ul>
{% for transfer in transfers.in %}
{% if transfer.amount > 0 %}
<li>
{{ transfer.amount / 100000000000 }} WOW
(<a href="https://wownero.club/transaction/{{ transfer.txid }}" target="_blank">{{ transfer.txid | shorten_address }}</a>)
- {{ transfer.timestamp | humanize }}
</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
<p>No WOW received yet. Show this post some love!</p>
{% endif %}
</div>
<div class="column content">
<h4>WOW Sent</h4>
{% if transfers.out %}
<ul>
{% for transfer in transfers.out %}
<li>
{{ transfer.amount / 100000000000 }} WOW
(<a href="https://wownero.club/transaction/{{ transfer.txid }}" target="_blank">{{ transfer.txid | shorten_address }}</a>)
- {{ transfer.timestamp | humanize }}
</li>
{% endfor %}
</ul>
{% else %}
<p>No payouts yet.</p>
{% endif %}
</div>
</div>
</section>
<h2>Payments</h2>
<p style="word-break:break-all;">Vote for this post by sending WOW to the following address:<br><i>{{ address }}</i></p>
{% if qr_code %}
<img src="data:image/png;base64,{{ qr_code }}" width=180 class="center"><br /><br />
{% endif %}
<h5>WOW Received</h5>
{% if transfers.in %}
<ul>
{% for transfer in transfers.in %}
<li style="word-break:break-all;list-style:none;">{{ transfer.amount / 100000000000 }} WOW (<a href="https://wownero.club/transaction/{{ transfer.txid }}" target="_blank">{{ transfer.txid }}</a>)</li>
{% endfor %}
</ul>
{% else %}
<p>No WOW received yet. Show this post some love!</p>
{% endif %}
<h5>WOW Sent</h5>
{% if transfers.out %}
<ul>
{% for transfer in transfers.out %}
<li>{{ transfer.amount / 100000000000 }} WOW (<a href="https://wownero.club/transaction/{{ transfer.txid }}" target="_blank">{{ transfer.txid }}</a>)</li>
{% endfor %}
</ul>
{% else %}
<p>No payouts yet.</p>
{% endif %}
<hr>
{% endif %}
@ -82,6 +55,13 @@
{{ post.show() }}
{% endif %}
{#
{% if "auth" in session %}
{% if session.auth.preferred_username == post.submitter %}
<hr><a href="{{ url_for('post.delete', id=post.id) }}"><button class="btn btn-danger">Delete Post</button></a>
{% endif %}
{% endif %}
#}
</div>
</div>

@ -4,56 +4,41 @@
<div class="container" style="text-align:center;">
<h1 class="title">Top Memes Last {{ days }} Days</h1>
<div class="title">
<h3>{% block title %}Top Posts{% endblock %}</h3>
</div>
<section class="section">
{% if posts %}
{% for row in posts | sort(attribute='received_wow', reverse=True) | batch(4) %}
<div class="columns">
{% for post in row %}
<div class="column">
<div class="card">
<div class="card-image">
<a href="{{ url_for('post.read', id=post.id) }}">
<img src="{{ url_for('post.uploaded_file', filename=post.thumbnail_name) }}" alt="Placeholder image">
</a>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<p class="title is-4">
<a href="{{ url_for('post.read', id=post.id) }}">{{ post.title }}</a>
</p>
<p class="subtitle is-6"><a href="/?submitter={{ post.submitter }}">{{ post.submitter }}</a></p>
</div>
</div>
<div class="content">
{{ post.text | truncate(60) }}
<p><strong>{{ post.received_wow }} WOW received</strong></p>
<time datetime="2016-1-1">{{ post.timestamp.year }}-{{ post.timestamp.month }}-{{ post.timestamp.day }} {{ post.timestamp.hour }}:{{ post.timestamp.minute }} UTC</time>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<table class="table table-striped">
<tr>
<th>Date</th>
<th>ID</th>
<th>Title</th>
<th>Submitter</th>
<th>Thumbnail</th>
<th>Amount</th>
</tr>
{% for post in posts %}
<tr>
<td>{{ post[1].timestamp.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ post[1].id }}</td>
<td><a href="{{ url_for('post.read', id=post[1].id) }}">{{ post[1].title }}</a></td>
<td><a href="/?submitter={{ post[1].submitter }}">{{ post[1].submitter }}</a></td>
<td>
<a href="{{ url_for('post.read', id=post[1].id) }}">
<img src="{{ url_for('post.uploaded_file', filename=post[1].get_thumbnail_name()) }}" width=200 style="max-height:200px;">
</a>
</td>
<td>{{ post[0] }} WOW</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>No posts yet!</p>
{% endif %}
</section>
{% if total_pages %}
<nav class="pagination is-centered pb-4" role="navigation" aria-label="pagination">
<ul class="pagination-list">
{% for p in range(1, total_pages + 1) %}
<a href="{% if request.args.submitter %}/?submitter={{ request.args.submitter }}&{% else %}/?{% endif %}page={{ p }}" class="pagination-link {% if p == page %}current-page-btn{% endif %}">{{ p }}</a>
{% endfor %}
</ul>
</nav>
{% endif %}
<hr>
<a href="{{ url_for('index') }}"><button class="btn btn-warning">Go Home</button></a>
</div>

@ -0,0 +1,77 @@
{% extends 'base.html' %}
{% block content %}
<div class="container" style="text-align:center;">
<h3>Stats</h3>
<div style="width: 100%; text-align: center;">
<div class="charts">
<canvas id="wow_wallet"></canvas>
<canvas id="wow_earnings"></canvas>
<canvas id="swap_stats"></canvas>
</div>
</div>
<script src="/static/js/Chart.bundle.min.js"></script>
<script>
var monero = '#f96b0e';
var set_title = function(t){
return {
display: true,
text: t,
fontColor: 'white',
}
}
{% for i in wow_txes %}
// console.log("{{ i['in'] }}")
{% endfor %}
{#
var ctx = document.getElementById('wow_wallet').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: [0, {% for i in wow_txes %}'{{ wow_txes[i].timestamp }}',{% endfor %}],
datasets: [{
label: 'Balance',
backgroundColor: wownero,
borderColor: wownero,
data: [
0, {% for i in wow_txes %}{{ wow_txes[i].total }},{% endfor %}
],
fill: false,
}]
},
options: {
title: set_title('Wownero Wallet Balance')
}
});
var ctx = document.getElementById('wow_earnings').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['WOW', 'USD'],
datasets: [{
label: 'Wownero Earnings',
backgroundColor: wownero,
borderColor: wownero,
data: [
{{ earnings['wow'] | from_atomic_wow }}, {{ earnings['wow_as_ausd'] | from_atomic_usd }}
],
fill: false,
}]
},
options: {
title: set_title('Wownero Earnings')
}
});
#}
</script>
</div>
{% endblock %}

@ -1,11 +1,7 @@
import pickle
from os import path, remove
from datetime import datetime, timedelta
from requests import post as r_post
from json import dumps
from flask import session, current_app
from suchwow.models import Moderator, Post
from suchwow.wownero import Wallet, from_atomic
from suchwow.models import Moderator
from suchwow import config
@ -23,7 +19,7 @@ def is_moderator(username):
def get_session_user():
if "auth" not in session or not session["auth"]:
return None
return session["auth"]["preferred_username"].strip()
return session["auth"]["preferred_username"]
def post_webhook(msg):
try:
@ -40,116 +36,3 @@ def post_webhook(msg):
return True
except:
return False
def get_latest_tipped_posts():
key_name = 'latest_tips'
posts = []
tipped_posts = rw_cache(key_name)
if not tipped_posts:
w = Wallet()
data = {}
for acc in w.accounts():
txes = w.transfers(acc)
if 'in' in txes:
for tx in txes['in']:
p = Post.select().where(
Post.account_index==acc
).first()
if p:
data[tx['timestamp']] = p
dates = sorted(data, reverse=True)
for d in dates:
if not data[d] in posts:
posts.append(data[d])
tipped_posts = rw_cache(key_name, posts)
return tipped_posts
def get_top_posters():
top_posters = {}
posts = rw_cache('top_posters')
if not posts:
posts = Post.select().where(Post.approved==True)
for post in posts:
transfers = []
incoming = Wallet().incoming_transfers(post.account_index)
if "transfers" in incoming:
for xfer in incoming["transfers"]:
transfers.append(from_atomic(xfer["amount"]))
total = sum(transfers)
if post.submitter not in top_posters:
top_posters[post.submitter] = {"amount": 0, "posts": []}
top_posters[post.submitter]["amount"] += float(total)
top_posters[post.submitter]["posts"].append(post)
rw_cache('top_posters', top_posters)
else:
top_posters = posts
return top_posters
def get_top_posts(days=1):
top_posts = []
try:
days = int(days)
except:
days = 1
if days not in [1, 3, 7, 30]:
days = 7
hours = 24 * days
diff = datetime.now() - timedelta(hours=hours)
key_name = f'top_posts_{str(hours)}'
posts = rw_cache(key_name)
if not posts:
posts = Post.select().where(
Post.approved==True,
Post.timestamp > diff
).order_by(
Post.timestamp.desc()
)
for post in posts:
p = post.show()
if isinstance(p['received_wow'], float):
top_posts.append(p)
posts = rw_cache(key_name, top_posts)
return posts
# Use hacky filesystem cache since i dont feel like shipping redis
def rw_cache(key_name, data=None, diff_seconds=3600):
pickle_file = path.join(config.DATA_FOLDER, f'{key_name}.pkl')
try:
if path.isfile(pickle_file):
mtime_ts = path.getmtime(pickle_file)
mtime = datetime.fromtimestamp(mtime_ts)
now = datetime.now()
diff = now - mtime
# If pickled data file is less than an hour old, load it and render page
# Otherwise, determine balances, build json, store pickled data, and render page
if diff.seconds < diff_seconds:
print(f'unpickling {key_name}')
with open(pickle_file, 'rb') as f:
pickled_data = pickle.load(f)
return pickled_data
else:
if data:
print(f'pickling {key_name}')
with open(pickle_file, 'wb') as f:
f.write(pickle.dumps(data))
return data
else:
return None
else:
if data:
print(f'pickling {key_name}')
with open(pickle_file, 'wb') as f:
f.write(pickle.dumps(data))
return data
else:
return None
except:
return None

@ -13,8 +13,8 @@ class Wallet(object):
self.host = config.WALLET_HOST
self.port = config.WALLET_PORT
self.proto = config.WALLET_PROTO
self.username = config.WALLET_RPC_USER
self.password = config.WALLET_RPC_PASS
self.username = config.WALLET_USER
self.password = config.WALLET_PASS
self.endpoint = '{}://{}:{}/json_rpc'.format(
self.proto, self.host, self.port
)
@ -53,8 +53,15 @@ class Wallet(object):
return self.make_wallet_rpc('query_key', {'key_type': 'mnemonic'})['key']
def accounts(self):
accounts = []
_accounts = self.make_wallet_rpc('get_accounts')
return [i['account_index'] for i in _accounts['subaddress_accounts']]
idx = 0
self.master_address = _accounts['subaddress_accounts'][0]['base_address']
for _acc in _accounts['subaddress_accounts']:
assert idx == _acc['account_index']
accounts.append(_acc['account_index'])
idx += 1
return accounts
def new_account(self, label=None):
_account = self.make_wallet_rpc('create_account', {'label': label})
@ -144,4 +151,4 @@ def from_atomic(amount):
return (Decimal(amount) * PICOWOW).quantize(PICOWOW)
def as_wownero(amount):
return float(Decimal(amount).quantize(PICOWOW))
return Decimal(amount).quantize(PICOWOW)

Loading…
Cancel
Save