Compare commits

...

43 Commits

Author SHA1 Message Date
lza_menace cd40c7a51d add links to post from mods view 2 years ago
lza_menace 65990ae26c save wallet after making changes 2 years ago
lza_menace 0a23715ef8 update tpls 2 years ago
lza_menace f85f8cbdbf dont err if file doesnt delete 2 years ago
lza_menace 1696c7fb58 hotfix 2 years ago
lza_menace aec88afc0d cleanup 2 years ago
lza_menace 3094205362 add audit logs 2 years ago
lza_menace 143d08f2f7 moving stuff around, cleaning up, adding mod area 2 years ago
lza_menace 0cab370e12 show list of active mods and their addresses 2 years ago
lza_menace 8c546a07eb prevent posting if wallet account is being reused 3 years ago
lza_menace af87610e60 wrap payout code in try/except block 3 years ago
lza_menace e908aaceda setup all-time stats 3 years ago
lza_menace 226e7bbe15 move user ban functionality to the database instead of config 3 years ago
lza_menace 6ef47009b8 mp4 and svg support 3 years ago
lza_menace 14a756690b updating flash colors w/ categories 3 years ago
lza_menace d0ff90dd5c document and improve repo a bit 3 years ago
lza_menace 8a2c66d3b6 update prod run script 3 years ago
lza_menace 017a230cc1 include latest tipped in cache load 3 years ago
lza_menace c4dc5cd49a only sweep if balance is enough 3 years ago
lza_menace 113af08502 load leaderboards into cache in advance 3 years ago
lza_menace ea02ed1185 i cant explain why it's not saving the post to_reddit 4 years ago
lza_menace 8f7c289f70 fix ref 4 years ago
lza_menace f797925d5e bug fix, store the fact we posted to reddit 4 years ago
lza_menace 7db1d41615 add js to have navbar buttons work 4 years ago
lza_menace 9045de9029 dont submit again 4 years ago
lza_menace 307d189967 add cli command to post to reddit for last x hours 4 years ago
lza_menace 490082cc09 just post to discord for now 4 years ago
lza_menace 09eebc719e dont trim, strip! 4 years ago
lza_menace 7bda8f8f30 add sleep stmt 4 years ago
lza_menace fbd2cc3c34 update prod run script 4 years ago
lza_menace 57f0bcc880 trim whitespace for dummies with an extra space in their name 4 years ago
lza_menace 2a7c618e9d only bind local port 4 years ago
lza_menace 00a97eb686 include meme flair when posting to reddit 4 years ago
lza_menace c46a817eed drop webhook on payout......too noisy 4 years ago
lza_menace 67d8121288 add reddit posting again 4 years ago
lza_menace 31e1af9426 donate addys 4 years ago
lza_menace c5d3977e0d fix ref 4 years ago
lza_menace 71b2cb4c15 revert to feed, add new favicoon 4 years ago
lza_menace f98a53c6ce fixing my bad fix. fuck fixing 4 years ago
lza_menace e60390b5c7 fix another broken link 4 years ago
lza_menace f1d9989b49 fix broken link 4 years ago
lza_menace be5bf5af76 redo default home screen, show recently tipped memes 4 years ago
lza_menace 9ec4d37448 bring back to bg 4 years ago

2
.gitignore vendored

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

@ -0,0 +1,15 @@
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.

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

@ -1,40 +1,99 @@
# SuchWow!
TBD
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.
## 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.
```
# 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
# 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
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# setup secrets in config file outside of git
cp suchwow/config.example.py suchwow/config.py
vim !$
# 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
```
# 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.

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

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

@ -1,20 +0,0 @@
#!/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"

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

@ -0,0 +1,29 @@
#!/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

@ -8,3 +8,4 @@ praw
qrcode
Pillow
arrow
python-dotenv

@ -3,29 +3,26 @@
set -e
set +x
# 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"
# mainnet wownero
export $(cat .env)
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 \
lalanza808/wownero:latest \
wownero-wallet-cli \
--daemon-address $DAEMON_URI \
--generate-new-wallet /root/wow \
--password $WALLET_PASS
fi
# setup rpc process
docker run --rm -d --name suchwow-wallet-rpc \
# run rpc process
docker run --restart=always -d --name suchwow-wallet-rpc \
-v $WALLET_PATH:/root \
-p 8888:8888 \
lalanza808/wownero \
lalanza808/wownero:latest \
wownero-wallet-rpc \
--daemon-address $DAEMON_URI \
--wallet-file /root/wow \

@ -1,21 +1,8 @@
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
from flask import Flask, request, session, redirect
from flask import render_template, flash, url_for
from flask import Flask
from flask_session import Session
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_activity
from suchwow.reddit import make_post
from suchwow.discord import post_discord_webhook
from suchwow import wownero, filters
from suchwow.routes import auth, post, profile, leaderboard, api, mod, main
from suchwow import filters, cli
app = Flask(__name__)
@ -26,118 +13,12 @@ Session(app)
app.register_blueprint(post.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(profile.bp)
app.register_blueprint(comment.bp)
app.register_blueprint(leaderboard.bp)
app.register_blueprint(api.bp)
app.register_blueprint(mod.bp)
app.register_blueprint(main.bp)
app.register_blueprint(filters.bp)
@app.route("/")
def index():
itp = 15
page = request.args.get("page", 1)
submitter = request.args.get("submitter", None)
content = request.args.get("content", None)
try:
page = int(page)
except:
flash("Wow, wtf hackerman. Cool it.")
page = 1
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
)
@app.route("/mod")
@moderator_required
def mod_queue():
posts = Post.select().where(Post.approved==False).order_by(Post.timestamp.asc())
return render_template("index.html", posts=posts)
@app.route("/about")
def about():
return render_template("about.html")
@app.errorhandler(404)
def not_found(error):
flash("nothing there, brah")
return redirect(url_for("index"))
@app.cli.command("init")
def init():
# create subdirs
for i in ["uploads", "db", "wallet"]:
makedirs(f"{config.DATA_FOLDER}/{i}", exist_ok=True)
# init db
db.create_tables([Post, Profile, Comment, Notification, Moderator])
@app.cli.command("create_accounts")
def create_accounts():
wallet = wownero.Wallet()
for post in Post.select():
if post.account_index not in wallet.accounts():
account = wallet.new_account()
print(f"Created account {account}")
@app.cli.command("payout_users")
def payout_users():
wallet = wownero.Wallet()
_fa = wownero.from_atomic
_aw = wownero.as_wownero
for post in Post.select():
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:
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)
if "tx_hash_list" in sweep:
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})")
@app.cli.command("add_admin")
@click.argument("username")
def add_admin(username):
if not Moderator.filter(username=username):
m = Moderator(username=username)
m.save()
print(f"Added {username}")
post_webhook(f"Moderator `{username}` added :ship_it_parrot: by console :black_flag:")
else:
print("That moderator already exists")
@app.cli.command("remove_admin")
@click.argument("username")
def remove_admin(username):
m = Moderator.filter(username=username).first()
if m:
m.delete_instance()
print(f"Deleted {username}")
post_webhook(f"Moderator `{username}` removed :excuseme: by console :black_flag:")
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.register_blueprint(cli.bp)
if __name__ == "__main__":

@ -0,0 +1,95 @@
from os import makedirs
import click
from flask import Blueprint, url_for, current_app
from suchwow.models import Post, Profile, Comment, Notification, db, Moderator, Ban, AuditEvent
from suchwow.utils.helpers import get_latest_tipped_posts
from suchwow.utils.helpers import get_top_posters, get_top_posts
from suchwow.reddit import make_post
from suchwow import wownero
from suchwow import config
bp = Blueprint('cli', 'cli', cli_group=None)
@bp.cli.command("init")
def init():
# create subdirs
for i in ["uploads", "db", "wallet"]:
makedirs(f"{config.DATA_FOLDER}/{i}", exist_ok=True)
# init db
db.create_tables([Post, Profile, Comment, Notification, Moderator, Ban, AuditEvent])
@bp.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
@bp.cli.command("create_accounts")
def create_accounts():
wallet = wownero.Wallet()
for post in Post.select():
if post.account_index not in wallet.accounts():
account = wallet.new_account()
print(f"Created account {account}")
@bp.cli.command("payout_users")
def payout_users():
wallet = wownero.Wallet()
_fa = wownero.from_atomic
_aw = wownero.as_wownero
for post in Post.select():
try:
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.05:
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)
if "tx_hash_list" in sweep:
amount = 0
for amt in sweep["amount_list"]:
amount += int(amt)
except Exception as e:
print(f"Failed because: {e}")
@bp.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")
@bp.cli.command("load_cache")
def load_cache():
current_app.logger.info('loading top posters into cache')
get_top_posters()
current_app.logger.info('done')
current_app.logger.info('loading latest tipped into cache')
get_latest_tipped_posts()
current_app.logger.info('done')
for i in [1, 3, 7, 30, 9999]:
current_app.logger.info(f'loading top posts last {i} days into cache')
get_top_posts(i)
current_app.logger.info('done')

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

@ -0,0 +1,45 @@
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)
SUPER_ADMIN = getenv('SUPER_ADMIN', 'lza_menace') # top dawg you cannot delete
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 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', 'svg', 'mp4'}
MAX_CONTENT_LENGTH = 32 * 1024 * 1024
TEMPLATES_AUTO_RELOAD = getenv('TEMPLATES_AUTO_RELOAD', True)

@ -1,6 +1,8 @@
from flask import Blueprint, current_app
from flask import Blueprint
from arrow import get as arrow_get
from suchwow.models import Moderator
bp = Blueprint('filters', 'filters')
@ -13,3 +15,10 @@ def shorten_address(a):
@bp.app_template_filter('humanize')
def humanize(d):
return arrow_get(d).humanize()
@bp.app_template_filter('is_moderator')
def is_moderator(s):
m = Moderator.select().where(Moderator.username == s)
if m:
return True
return False

@ -1,12 +1,25 @@
from peewee import *
from random import choice
from os import path
from datetime import datetime
from peewee import *
from PIL import Image
from suchwow import wownero
from suchwow import config
db = SqliteDatabase(f"{config.DATA_FOLDER}/db/sqlite.db")
db = SqliteDatabase(f"{config.DATA_FOLDER}/sqlite.db")
ban_reasons = [
'you smell bad',
'didnt pass the vibe check, homie',
'your memes are bad and you should feel bad',
'i just dont like you'
]
def get_ban_reason():
return choice(ban_reasons)
class Post(Model):
id = AutoField()
@ -123,3 +136,21 @@ class Notification(Model):
class Meta:
database = db
class Ban(Model):
id = AutoField()
user = ForeignKeyField(Profile)
reason = TextField()
timestamp = DateTimeField(default=datetime.now)
class Meta:
database = db
class AuditEvent(Model):
id = AutoField()
user = ForeignKeyField(Profile)
timestamp = DateTimeField(default=datetime.now)
action = CharField()
class Meta:
database = db

@ -13,6 +13,7 @@ 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:
@ -20,6 +21,7 @@ class Reddit(object):
title=title,
url=url,
resubmit=False,
flair_id=self.meme_flair_id
)
return submission
except:
@ -33,6 +35,9 @@ 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,4 +1,5 @@
from flask import jsonify, Blueprint, url_for, request, abort
from suchwow.models import Post
from suchwow import wownero

@ -1,6 +1,8 @@
import requests
from uuid import uuid4
import requests
from flask import session, redirect, url_for, request, Blueprint
from suchwow import config
@ -21,7 +23,7 @@ def login():
@bp.route("/logout")
def logout():
session["auth"] = None
return redirect(url_for("index"))
return redirect(url_for("main.index"))
@bp.route("/auth/")
def auth():
@ -66,4 +68,4 @@ def auth():
# user can now visit /secret
session["auth"] = user_profile
return redirect(url_for("index"))
return redirect(url_for("main.index"))

@ -1,30 +0,0 @@
from flask import render_template, Blueprint, flash
from flask import request, redirect, url_for, session
from suchwow.models import Post, Comment, Profile
from suchwow.utils.decorators import login_required
bp = Blueprint("comment", "comment")
@bp.route("/comment/create/post/<post_id>", methods=["GET", "POST"])
@login_required
def create(post_id):
if not Post.filter(id=post_id):
flash("WTF, that post doesn't exist. Stop it, hackerman.")
return redirect(url_for("index"))
if request.method == "POST":
comment_text = request.form.get("comment")
if len(comment_text) > 300:
flash("WTF, too many characters to post, asshole.")
return redirect(request.url)
commenter = Profile.get(username=session["auth"]["preferred_username"])
post = Post.get(id=post_id)
c = Comment(
comment=comment_text,
commenter=commenter,
post=post,
)
c.save()
return redirect(url_for("post.read", id=post_id))
return render_template("comment/create.html")

@ -1,68 +1,25 @@
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
from flask import render_template, Blueprint, request
from suchwow.utils.helpers import get_top_posters, get_top_posts
bp = Blueprint("leaderboard", "leaderboard")
@bp.route("/leaderboards/top_posters")
def 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 = 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": []}
top_posters[post.submitter]["amount"] += float(total)
top_posters[post.submitter]["posts"].append(post)
rw_cache('top_posters', top_posters)
else:
top_posters = posts
top_posters = get_top_posters()
return render_template("leaderboard.html", posters=top_posters)
@bp.route("/leaderboards/top_posts")
def top_posts():
top_posts = []
days = request.args.get('days', 1)
try:
days = int(days)
except:
days = 1
if days not in [1, 3, 7, 30]:
if days not in [1, 3, 7, 30, 9999]:
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)
posts = get_top_posts(days)
return render_template("post/top.html", posts=posts, days=days)

@ -0,0 +1,50 @@
from math import ceil
from flask import Blueprint, request, render_template, flash
from suchwow.models import Post, Profile, Moderator
from suchwow.utils.helpers import get_latest_tipped_posts
bp = Blueprint('main', 'main')
@bp.route("/")
def index():
itp = 15
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:
flash("Wow, wtf hackerman. Cool it.", "is-danger")
page = 1
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"
)
@bp.route("/about")
def about():
mods = Profile.select().join(Moderator, on=(Profile.username == Moderator.username))
return render_template("about.html", mods=mods)

@ -0,0 +1,114 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request
from suchwow.models import AuditEvent, Post, Profile, Moderator, Ban, get_ban_reason
from suchwow.utils.decorators import moderator_required
from suchwow.utils.helpers import get_session_user, audit_event
from suchwow import config
bp = Blueprint("mod", "mod")
@bp.route('/mods')
@moderator_required
def main():
live_posts = Post.select().where(Post.approved == True).count()
pending_posts = Post.select().where(Post.approved == False).count()
active_posters = Profile.select().join(Post, on=Post.submitter == Profile.username).distinct().count()
mods = Moderator.select().count()
return render_template(
'mod/main.html',
live_posts=live_posts,
pending_posts=pending_posts,
active_posters=active_posters,
mods=mods
)
@bp.route('/mods/pending')
@moderator_required
def pending_posts():
posts = Post.select().where(Post.approved == False).order_by(Post.timestamp.asc())
if not posts:
flash('no posts pending', 'is-warning')
return redirect(url_for('mod.main'))
return render_template('mod/posts.html', posts=posts)
@bp.route('/mods/manage', methods=['GET', 'POST'])
@moderator_required
def manage_mods():
to_delete = request.args.get('delete')
if to_delete:
m = Moderator.select().where(Moderator.username == to_delete).first()
if not m:
flash('No moderator exists with that name', 'is-danger')
elif m.username == get_session_user():
flash('Cannot remove yourself.', 'is-danger')
elif m.username == config.SUPER_ADMIN:
flash('Cannot delete super admin you son-of-a-bitch.', 'is-danger')
else:
m.delete_instance()
audit_event(f'Deleted {to_delete} from mods')
flash(f'Removed {to_delete} from mods!', 'is-success')
return redirect(url_for('mod.manage_mods'))
if request.method == 'POST':
to_add = request.form.get('username', None)
if to_add:
u = Profile.select().where(Profile.username == to_add).first()
if not u:
flash('That user does not appear to exist (no profile setup yet)', 'is-danger')
elif Moderator.select().where(Moderator.username == to_add).first():
flash(f'{to_add} is already a mod, ya dingus.', 'is-warning')
else:
m = Moderator(username=to_add)
m.save()
audit_event(f'Added {to_add} to mods')
flash(f'Added {to_add} to mods!', 'is-success')
mods = Profile.select().join(Moderator, on=(Profile.username == Moderator.username))
return render_template('mod/manage.html', mods=mods)
@bp.route('/mods/bans', methods=['GET', 'POST'])
@moderator_required
def manage_bans():
to_delete = request.args.get('delete')
if to_delete:
ban = Ban.select().join(Profile).where(Profile.username == to_delete).first()
if not ban:
flash('No ban exists for that user', 'is-danger')
elif ban.user == get_session_user():
flash('Cannot ban yourself.', 'is-danger')
elif ban.user == config.SUPER_ADMIN:
flash('Cannot ban super admin you son-of-a-bitch.', 'is-danger')
else:
ban.delete_instance()
audit_event(f'Removed ban on {to_delete}')
flash(f'Unbanned {to_delete}!', 'is-success')
return redirect(url_for('mod.manage_bans'))
if request.method == 'POST':
to_add = request.form.get('username', None)
if to_add:
u = Profile.select().where(Profile.username == to_add).first()
if not u:
flash('That user does not appear to exist (no profile setup yet)', 'is-danger')
elif Ban.select().join(Profile).where(Profile.username == to_add).first():
flash(f'{to_add} is already banned, ya dingus.', 'is-warning')
elif to_add == config.SUPER_ADMIN:
flash('Cannot ban the super admin you son-of-a-bitch.', 'is-danger')
else:
reason = request.form.get('reason')
if not reason:
reason = get_ban_reason()
ban = Ban(user=u, reason=reason)
ban.save()
audit_event(f'Banned {to_add} ({reason})')
flash(f'Banned {to_add}!', 'is-success')
bans = Ban.select()
return render_template('mod/bans.html', bans=bans)
@bp.route('/mods/logs')
@moderator_required
def view_logs():
events = AuditEvent.select().order_by(AuditEvent.timestamp.desc()).limit(50)
return render_template('mod/logs.html', logs=events)

@ -1,19 +1,18 @@
from datetime import datetime, timedelta
from os import path, remove
from io import BytesIO
from base64 import b64encode
from secrets import token_urlsafe
from qrcode import make as qrcode_make
from flask import render_template, Blueprint, request, session, flash
from flask import render_template, Blueprint, request, flash
from flask import send_from_directory, redirect, url_for, current_app
from werkzeug.utils import secure_filename
from secrets import token_urlsafe
from suchwow import wownero
from suchwow import config
from suchwow.models import Post, Comment
from suchwow.models import Post, Profile, Comment, Ban
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.reddit import make_post
from suchwow.utils.helpers import audit_event
from suchwow.discord import post_discord_webhook
@ -28,7 +27,7 @@ def read(id):
post = Post.get(id=id)
if not post.approved:
if not is_moderator(get_session_user()):
flash("That post has not been approved.")
flash("That post has not been approved.", "is-warning")
return redirect("/")
if wallet.connected:
address = wallet.get_address(account=post.account_index)
@ -47,32 +46,33 @@ def read(id):
qr_code=qr_code
)
else:
flash("No meme there, brah")
return redirect(url_for("index"))
flash("No meme there, brah", "is-warning")
return redirect(url_for("main.index"))
@bp.route("/post/create", methods=["GET", "POST"])
@login_required
@profile_required
def create():
if request.method == "POST":
submitter = get_session_user()
if submitter in config.BANNED_USERS:
reason = config.BANNED_USERS[submitter]
flash(f"You can't post for now: {reason}")
u = Profile.filter(username=submitter)
banned = Ban.filter(user=u).first()
if banned:
flash(f"You can't post: {banned.reason}", "is-danger")
return redirect("/")
if request.method == "POST":
post_title = request.form.get("title")
# check if the post request has the file part
if "file" not in request.files:
flash("You didn't upload a caliente meme, bro! You're fuckin up!")
flash("You didn't upload a caliente meme, bro! You're fuckin up!", "is-danger")
return redirect(request.url)
file = request.files["file"]
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == "":
flash("You didn't upload a caliente meme, bro! You're fuckin up!")
flash("You didn't upload a caliente meme, bro! You're fuckin up!", "is-danger")
return redirect(request.url)
if post_title == "":
flash("You didn't give your meme a spicy title, bro! You're fuckin up!")
flash("You didn't give your meme a spicy title, bro! You're fuckin up!", "is-danger")
return redirect(request.url)
if file and allowed_file(file.filename):
filename = "{}-{}".format(
@ -85,8 +85,12 @@ def create():
try:
wallet = wownero.Wallet()
account_index = wallet.new_account()
in_use = Post.select().where(Post.account_index == account_index).first()
if in_use:
flash("Suchwow wallet is fucked up! Try again later.", "is-danger")
return redirect(request.url)
except:
flash("Suchwow wallet is fucked up! Try again later.")
flash("Suchwow wallet is fucked up! Try again later.", "is-danger")
return redirect(request.url)
post = Post(
title=post_title,
@ -99,9 +103,9 @@ def create():
post.save()
post.save_thumbnail()
url = url_for('post.read', id=post.id, _external=True)
post_webhook(f"New post :doge2: [{post.id}]({url}) by `{submitter}` :neckbeard:")
flash("New post created and pending approval!")
return redirect(url_for("index"))
audit_event(f'Created new post {post.id}')
flash("New post created and pending approval!", "is-success")
return redirect(url_for("main.index"))
return render_template("post/create.html")
@bp.route("/post/<id>/approve")
@ -113,16 +117,14 @@ def approve(id):
if not post.approved:
post.approved = True
post.save()
post_webhook(f"Post [{post.id}]({url}) approved :white_check_mark: by `{get_session_user()}` :fieri_parrot:")
flash("Approved")
flash("Approved", "is-success")
audit_event(f'Approved post {post.id}')
if current_app.config["DEBUG"] is False:
if not post.to_discord:
_d = post_discord_webhook(post)
post_webhook(f"Post [{post.id}]({url}) submitted :dab_parrot: to Discord.")
return redirect(url_for("mod_queue"))
post_discord_webhook(post)
return redirect(url_for("mod.pending_posts"))
else:
flash("You can't approve this")
return redirect(url_for("index"))
flash("You can't approve this", "is-success")
return redirect(url_for("main.index"))
@bp.route("/post/<id>/delete")
@login_required
@ -135,20 +137,23 @@ def delete(id):
if user == post.submitter or is_mod:
save_path_base = path.join(current_app.config["DATA_FOLDER"], "uploads")
save_path = path.join(save_path_base, post.image_name)
try:
remove(save_path)
except:
pass
audit_event(f'Deleted post {post.id}')
post.delete_instance()
post_webhook(f"Post {post.id} deleted :dumpsterfire: by `{user}` :godmode:")
flash("Deleted that shit, brah!")
flash("Deleted that shit, brah!", "is-success")
if is_mod:
return redirect(url_for("mod_queue"))
return redirect(url_for("mod.pending_posts"))
else:
return redirect(url_for("index"))
return redirect(url_for("main.index"))
else:
flash("You can't delete a meme you don't own, brah")
flash("You can't delete a meme you don't own, brah", "is-warning")
return redirect(url_for("post.read", id=post.id))
else:
flash("No meme there, brah")
return redirect(url_for("index"))
flash("No meme there, brah", "is-warning")
return redirect(url_for("main.index"))
@bp.route("/uploads/<path:filename>")
def uploaded_file(filename):

@ -26,7 +26,7 @@ def edit():
profile.save()
return redirect(request.args.get("redirect", "/"))
else:
flash("WTF bro, that's not a valid Wownero address")
flash("WTF bro, that's not a valid Wownero address", "is-warning")
return redirect(request.url)
if profile_exists:
profile = Profile.get(username=un)

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

@ -11,6 +11,12 @@
<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>
{% for mod in mods %}
<p><strong>{{ mod.username }}</strong> - {{ mod.address or '? (update your profile, bub!)' }}</p>
{% endfor %}
<br />
<br />
<br />
</div>
</div>

@ -20,7 +20,7 @@
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="/static/wownero_logo.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) }}" />
@ -55,10 +55,10 @@
{% include 'navbar.html' %}
{% with messages = get_flashed_messages() %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for message in messages %}
<div class="notification is-danger container is-light">
{% for category, message in messages %}
<div class="notification {{ category | default('is-danger') }} container is-light">
{{ message }}
</div>
{% endfor %}
@ -70,13 +70,46 @@
<!-- Page Content -->
{% block content %} {% endblock %}
<div style="padding-bottom:10em;"></div>
<!-- Footer -->
{% block 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>
{% endblock %}
<!-- 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>
</body>
</html>

@ -1,19 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="container" style="width:40%;">
<div class="submit">
<h1>Leave a Comment</h1>
<form method=post enctype=multipart/form-data class="form-horizontal">
<div class="form-group">
<label class="sr-only" for="inlineFormInput">Text</label>
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="inlineFormInput" placeholder="Comment text (max 300 chars)" name="comment">
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">Submit</button>
</div>
</form>
</div>
</div>
{% endblock %}

@ -4,19 +4,32 @@
<div class="container" style="text-align:center;">
<h1 class="title">Latest Memes</h1>
<h1 class="title">{% if title %}{{ title }}{% else %}Latest Memes{% endif %}</h1>
{% if request.args.content == 'latest_tipped' %}
<a href="/">View Latest Memes</a>
{% else %}
<a href="/?content=latest_tipped">View Latest Tipped Posts</a>
{% endif %}
<section class="section">
{% if posts %}
{% for row in posts | batch(4) %}
<div class="columns">
{% for post in row %}
{% set post = post.show() %}
{% for p in row %}
{% set post = p.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">
{% if p.get_image_path().endswith('mp4') %}
<video style="max-height: 100vh!important;" controls>
<source src="{{ url_for('post.uploaded_file', filename=p.image_name) }}" type="video/mp4">
Your browser does not support the video tag.
</video>
{% else %}
<img alt="SuchWow #{{ post.id }} - {{ post.title }} by {{ post.submitter }}" src="{{ url_for('post.uploaded_file', filename=post.thumbnail_name) }}" />
{% endif %}
</a>
</div>
<div class="card-content">

@ -26,7 +26,7 @@
{% endif %}
<hr>
<a href="{{ url_for('index') }}"><button class="btn btn-warning">Go Home</button></a>
<a href="{{ url_for('main.index') }}"><button class="btn btn-warning">Go Home</button></a>
</div>

@ -0,0 +1,51 @@
{% extends 'base.html' %}
{% block content %}
<div class="container" style="text-align:center;">
<h1 class="title">Manage Mods</h1>
<section class="section">
{% for ban in bans %}
<article class="message" style="width: 30%; margin: 2em auto;">
<div class="message-header">
<p>{{ ban.user.username }}</p>
<a href="?delete={{ ban.user.username }}" class="delete"></a>
</div>
<div class="message-body">
{{ ban.reason }}
</div>
</article>
{% endfor %}
</section>
<div class="container" style="text-align:left; width: 30%;">
<form method="post">
<div class="field">
<label class="label">Ban User</label>
<div class="control">
<input class="input" type="text" placeholder="Username" name="username">
</div>
</div>
<div class="field">
<label class="label">Reason</label>
<div class="control">
<textarea class="textarea" placeholder="Reason for ban" name="reason"></textarea>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Submit</button>
</div>
<div class="control">
<button class="button is-link is-light">Cancel</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block footer %}{% endblock %}

@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block content %}
<div class="container" style="text-align:center;">
<h1 class="title">View Logs</h1>
<section class="section">
{% for log in logs %}
<article class="message" style="width: 40%; margin: 1em auto;">
<div class="message-header">
<p>{{ log.user.username }} - {{ log.timestamp | humanize }} </p>
</div>
<div class="message-body">
{{ log.action }}
</div>
</article>
{% endfor %}
</section>
</div>
{% endblock %}
{% block footer %}{% endblock %}

@ -0,0 +1,72 @@
{% extends 'base.html' %}
{% block content %}
<div class="container content" style="padding-top: 4em;">
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">Live Memes</p>
<p class="title">{{ live_posts }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Pending Memes</p>
<p class="title">{{ pending_posts }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Active Posters</p>
<p class="title">{{ active_posters }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Mods</p>
<p class="title">{{ mods }}</p>
</div>
</div>
</nav>
<div class="tabs is-toggle is-fullwidth">
<ul>
<li>
<a href="{{ url_for('mod.pending_posts') }}">
<span class="icon is-small"></span>
<span>Manage Queue</span>
</a>
</li>
<li>
<a href="{{ url_for('mod.manage_bans') }}">
<span class="icon is-small"></span>
<span>Manage Bans</span>
</a>
</li>
<li>
<a href="{{ url_for('mod.manage_mods') }}">
<span class="icon is-small"></span>
<span>Manage Mods</span>
</a>
</li>
<li>
<a href="{{ url_for('mod.view_logs') }}">
<span class="icon is-small"></span>
<span>View Logs</span>
</a>
</li>
</ul>
</div>
<figure>
<img src="https://suchwow.xyz/uploads/pg1VwHJWeKT5dWXy-wowcomfysemifinal.gif">
</figure>
<div style="padding-bottom:10em;"></div>
</div>
{% endblock %}
{% block footer %}{% endblock %}

@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% block content %}
<div class="container" style="text-align:center;">
<h1 class="title">Manage Mods</h1>
<section class="section">
{% for mod in mods %}
<div class="block">
<span class="tag is-large">
{{ mod.username }}
<a href="?delete={{ mod.username }}" class="delete"></a>
</span>
</div>
{% endfor %}
</section>
<div class="container" style="text-align:left; width: 30%;">
<form method="post">
<div class="field">
<label class="label">Add New Mod</label>
<div class="control">
<input class="input" type="text" placeholder="Username" name="username">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Submit</button>
</div>
<div class="control">
<button class="button is-link is-light">Cancel</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block footer %}{% endblock %}

@ -0,0 +1,61 @@
{% extends 'base.html' %}
{% block content %}
<div class="container" style="text-align:center;">
<h1 class="title">Pending Posts</h1>
<section class="section">
{% if posts %}
{% for row in posts | batch(4) %}
<div class="columns">
{% for p in row %}
{% set post = p.show() %}
<div class="column">
<div class="card">
<div class="card-image">
<a href="{{ url_for('post.read', id=p.id) }}">
{% if p.get_image_path().endswith('mp4') %}
<video style="max-height: 100vh!important;" controls>
<source src="{{ url_for('post.uploaded_file', filename=p.image_name) }}" type="video/mp4">
Your browser does not support the video tag.
</video>
{% else %}
<img alt="SuchWow #{{ post.id }} - {{ post.title }} by {{ post.submitter }}" src="{{ url_for('post.uploaded_file', filename=post.thumbnail_name) }}" />
{% endif %}
</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=p.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) }}
<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>
<footer class="card-footer">
<a href="{{ url_for('post.approve', id=post.id) }}" class="card-footer-item" style="color:green;"><strong>Approve</strong></a>
<a href="{{ url_for('post.delete', id=post.id) }}" class="card-footer-item" style="color:red;"><strong>Deny</strong></a>
</footer>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
<p>No posts pending!</p>
{% endif %}
</section>
</div>
{% endblock %}
{% block footer %}{% endblock %}

@ -13,7 +13,7 @@
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{{ url_for('index') }}">Home</a>
<a class="navbar-item" href="{{ url_for('main.index') }}">Home</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">Leaderboards</a>
@ -23,12 +23,15 @@
<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>
<a class="navbar-item" href="{{ url_for('leaderboard.top_posts') }}?days=9999">Top Memes All Time</a>
</div>
</div>
<a class="navbar-item" href="{{ url_for('about') }}">About</a>
<a class="navbar-item" href="{{ url_for('main.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>
{% if 'auth' in session %}
{% if session.auth.preferred_username | is_moderator %}
<a class="navbar-item" style="color: orange;" href="{{ url_for('mod.main') }}">Mods!</a>
{% endif %}
{% endif %}
</div>
@ -69,7 +72,7 @@
<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>
<a class="nav-link" href="{{ url_for('main.about') }}">About</a>
</li>
{% if session.auth == None %}
<li class="nav-item">

@ -9,25 +9,63 @@
<ol>
<li>no super low effort memes</li>
<li>no nsfl and nsfw</li>
<li>must pertain to wownero or crypto in general</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>
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="inlineFormInput" placeholder="Title" name="title">
<div class="field">
<label class="label">Title</label>
<div class="control">
<input class="input" type="text" placeholder="So there I was..." name="title">
</div>
<div class="form-group">
<label class="sr-only" for="inlineFormInput">Text</label>
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="inlineFormInput" placeholder="Text (optional)" name="text">
</div>
<div class="form-group">
<input type=file name=file>
<div class="field">
<label class="label">Text</label>
<div class="control">
<textarea class="textarea" placeholder="balls deep in a jar of mayonnaise" name="text"></textarea>
</div>
</div>
<div id="file-js-example" class="file has-name">
<label class="file-label">
<input class="file-input" type="file" name="file">
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label">
Upload a spicy meme
</span>
</span>
<span class="file-name">
No file uploaded
</span>
</label>
</div>
<div class="field mt-4">
<div class="control">
<label class="checkbox">
<input type="checkbox">
I agree to be hella based
</label>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Submit</button>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">Submit</button>
</div>
</form>
</div>
</div>
<script>
const fileInput = document.querySelector('#file-js-example input[type=file]');
fileInput.onchange = () => {
if (fileInput.files.length > 0) {
const fileName = document.querySelector('#file-js-example .file-name');
fileName.textContent = fileInput.files[0].name;
}
}
</script>
{% endblock %}

@ -21,11 +21,19 @@
<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>
<a href="{{ url_for('post.approve', id=post.id) }}" class="button is-success">Approve</a>
<a href="{{ url_for('post.delete', id=post.id) }}" class="button is-danger">Reject</a>
{% endif %}
<p class="mt-2">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;"> -->
{% if post.get_image_path().endswith('mp4') %}
<video style="max-height: 100vh!important;" controls>
<source src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" type="video/mp4">
Your browser does not support the video tag.
</video>
{% else %}
<img alt="SuchWow #{{ post.id }} - {{ post.title }} by {{ post.submitter }}" src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" />
{% 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>

@ -10,6 +10,7 @@
{% if posts %}
{% for row in posts | sort(attribute='received_wow', reverse=True) | batch(4) %}
<div class="columns">
{% if loop.index < 15 %}
{% for post in row %}
<div class="column">
<div class="card">
@ -37,6 +38,7 @@
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endfor %}
{% else %}

@ -20,8 +20,8 @@ def moderator_required(f):
if m:
return f(*args, **kwargs)
else:
flash("You are not a moderator")
return redirect(url_for("index"))
flash("You are not a moderator", "is-warning")
return redirect(url_for("main.index"))
return decorated_function
def profile_required(f):

@ -1,11 +1,11 @@
import pickle
from os import path, remove
from os import path
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 flask import session
from suchwow.models import Moderator, Post, AuditEvent, Profile
from suchwow.wownero import Wallet, from_atomic
from suchwow import config
@ -20,47 +20,100 @@ def is_moderator(username):
else:
return False
def get_profile():
p = Profile.filter(username=get_session_user()).first()
return p
def audit_event(event):
e = AuditEvent(user=get_profile(), action=event)
e.save()
def get_session_user():
if "auth" not in session or not session["auth"]:
return None
return session["auth"]["preferred_username"]
def post_webhook(msg):
try:
if current_app.config["DEBUG"]:
msg = "[DEBUG] " + msg
data = {
"text": msg,
"channel": config.MM_CHANNEL,
"username": config.MM_USERNAME,
"icon_url": config.MM_ICON
}
res = r_post(config.MM_ENDPOINT, data=dumps(data))
res.raise_for_status()
return True
except:
return False
return session["auth"]["preferred_username"].strip()
def get_activity():
posts = Post.select()
def get_latest_tipped_posts():
key_name = 'latest_tips'
posts = []
tipped_posts = rw_cache(key_name)
if not tipped_posts:
w = Wallet()
data = {}
for p in posts:
data[p.timestamp] = {'type': 'post', 'post': p}
for tx in w.incoming_transfers(p.account_index):
if 'timestamp' in tx:
data[tx['timestamp']] = {'type': 'tx', 'post': p}
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)
new_data = []
for d in dates:
new_data.append(data[d]['post'])
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
# stupid magic number bcuz fuck it
if days not in [1, 3, 7, 30, 9999]:
days = 7
hours = 24 * days
diff = datetime.now() - timedelta(hours=hours)
key_name = f'top_posts_{str(hours)}'
return new_data[0:20]
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):
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):
@ -70,7 +123,7 @@ def rw_cache(key_name, data=None):
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 < 3600:
if diff.seconds < diff_seconds:
print(f'unpickling {key_name}')
with open(pickle_file, 'rb') as f:
pickled_data = pickle.load(f)

@ -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_USER
self.password = config.WALLET_PASS
self.username = config.WALLET_RPC_USER
self.password = config.WALLET_RPC_PASS
self.endpoint = '{}://{}:{}/json_rpc'.format(
self.proto, self.host, self.port
)
@ -33,13 +33,17 @@ class Wallet(object):
r = requests.get(
self.endpoint,
data=json.dumps({'method': method, 'params': params}),
auth=self.auth
auth=self.auth,
timeout=10
)
if 'error' in r.json():
return r.json()['error']
else:
return r.json()['result']
def store(self):
self.make_wallet_rpc('store')
def height(self):
return self.make_wallet_rpc('get_height', {})
@ -53,18 +57,12 @@ 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')
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
return [i['account_index'] for i in _accounts['subaddress_accounts']]
def new_account(self, label=None):
_account = self.make_wallet_rpc('create_account', {'label': label})
self.store()
return _account['account_index']
def addresses(self, account, addr_indices=None):
@ -90,6 +88,7 @@ class Wallet(object):
def new_address(self, account, label=None):
data = {'account_index': account, 'label': label}
_address = self.make_wallet_rpc('create_address', data)
self.store()
return (_address['address_index'], _address['address'])
def transfers(self, account, address_indices=[]):
@ -120,6 +119,7 @@ class Wallet(object):
'ring_size': 22
}
transfer = self.make_wallet_rpc('transfer', data)
self.store()
return transfer
def sweep_all(self, account, dest_address):
@ -128,6 +128,7 @@ class Wallet(object):
'account_index': account,
}
sweep = self.make_wallet_rpc('sweep_all', data)
self.store()
return sweep
def incoming_transfers(self, account, transfer_type='all', verbose=True):

Loading…
Cancel
Save