Compare commits
43 Commits
@ -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
|
@ -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,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)
|
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 174 KiB |
@ -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 %}
|
@ -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 %}
|
Loading…
Reference in New Issue