Compare commits

...

18 Commits

1
.gitignore vendored

@ -7,3 +7,4 @@ __pycache__
*sql *sql
flask_session flask_session
.env .env
.env.dev

@ -0,0 +1,82 @@
#!/usr/bin/env python3
# export suchwow data for the purpose of importing into new model definitions
import pickle
from suchwow.models import Post, Moderator, Profile, Ban, AuditEvent
from suchwow import wownero
wallet = wownero.Wallet()
if not wallet.connected:
print('Wallet not connected')
exit()
all_posts = Post.select().order_by(Post.timestamp.asc())
all_mods = Moderator.select()
all_profiles = Profile.select()
all_bans = Ban.select()
all_audits = AuditEvent.select()
all_data = {
'posts': list(),
'moderators': list(),
'profiles': list(),
'bans': list(),
'auditevents': list()
}
for post in all_posts:
post_data = {
'id': post.id,
'title': post.title,
'text': post.text,
'submitter': post.submitter,
'image_name': post.image_name,
'readonly': post.readonly,
'hidden': post.hidden,
'account_index': post.account_index,
'address_index': post.address_index,
'timestamp': post.timestamp,
'reddit_url': post.reddit_url,
'to_reddit': post.to_reddit,
'to_discord': post.to_discord,
'approved': post.approved,
'txes': wallet.make_wallet_rpc('get_transfers', {
'account_index': post.account_index,
'subaddr_indices': [],
'in': True,
'out': True
})
}
txes = 0
all_data['posts'].append(post_data)
if 'in' in post_data['txes']:
txes = len(post_data['txes']['in'])
print(f'Exporting post {post.id}. Found {txes} txes')
for mod in all_mods:
all_data['moderators'].append(mod.username)
for profile in all_profiles:
all_data['profiles'].append({
'username': profile.username,
'address': profile.address
})
for ban in all_bans:
all_data['bans'].append({
'username': ban.user.username,
'reason': ban.reason,
'timestamp': ban.timestamp
})
for event in all_audits:
all_data['auditevents'].append({
'username': event.user.username,
'timestamp': event.timestamp,
'action': event.action
})
with open('data/migrate_data.pkl', 'wb') as f:
f.write(pickle.dumps(all_data))

@ -0,0 +1,107 @@
#!/usr/bin/env python3
# import pickled suchwow data for the purpose of importing into new model definitions
import pickle
from datetime import datetime
from suchwow._models import User, Post, AuditEvent, TipSent, TipReceived, Vote
from suchwow import wownero
wallet = wownero.Wallet()
all_data = dict()
if not wallet.connected:
print('Wallet not running and connected')
exit()
with open('data/migrate_data.pkl', 'rb') as f:
all_data = pickle.load(f)
# first import users from old profiles
for user in all_data['profiles']:
if not User.select().where(User.username == user['username']).first():
u = User(
username=user['username'],
address=user['address'],
)
u.save()
print(f'Added user {u.username}')
for post in all_data['posts']:
if not Post.select().where(Post.id == post['id']).first():
user = User.get(username=post['submitter'])
account_idx = 0
address_idx, address = wallet.new_address(account_idx)
print(f'Saving post {post["id"]} for user {user.username} (account {account_idx}, address_idx {address_idx}, {address}')
Post.create(
id=post['id'],
title=post['title'],
text=post['text'],
user=user,
image_name=post['image_name'],
account_index=account_idx,
address_index=address_idx,
address=address,
timestamp=post['timestamp'],
approved=post['approved']
)
if 'in' in post['txes']:
p = Post.get(post['id'])
for tx in post['txes']['in']:
amount = sum(tx['amounts'])
received = TipReceived.select().where(
TipReceived.txid == tx['txid'],
TipReceived.amount == amount,
TipReceived.post == p
).first()
if not received:
TipReceived.create(
post=p,
timestamp=datetime.utcfromtimestamp(tx['timestamp']),
txid=tx['txid'],
amount=amount,
fee=tx['fee']
)
print(f'Saving received tip txid {tx["txid"]}')
if 'out' in post['txes']:
p = Post.get(post['id'])
for tx in post['txes']['out']:
if not TipSent.select().where(TipSent.txid == tx['txid']).first():
TipSent.create(
from_user=p.user,
to_user=p.user,
txid=tx['txid'],
timestamp=datetime.utcfromtimestamp(tx['timestamp']),
amount=tx['amount'],
fee=tx['fee']
)
print(f'Saving sent tip txid {tx["txid"]}')
for mod in all_data['moderators']:
u = User.get(User.username == mod)
if not u.moderator:
u.moderator = True
u.save()
print(f'Updated {u.username} as moderator')
for ban in all_data['bans']:
u = User.get(User.username == ban['username'])
if not u.banned:
u.banned = True
u.ban_reason = ban['reason']
u.ban_timestamp = ban['timestamp']
u.save()
print(f'Banned {u.username} ({u.ban_reason})')
for event in all_data['auditevents']:
if not AuditEvent.select().where(AuditEvent.timestamp == event['timestamp']):
u = User.get(User.username == event['username'])
AuditEvent.create(
user=u,
action=event['action'],
timestamp=event['timestamp']
)
print(f'Saved audit event ({u.username} -> {event["action"]} @ {event["timestamp"]}')

@ -9,3 +9,4 @@ qrcode
Pillow Pillow
arrow arrow
python-dotenv python-dotenv
lorem

@ -21,7 +21,7 @@ fi
# run rpc process # run rpc process
docker run --restart=always -d --name suchwow-wallet-rpc \ docker run --restart=always -d --name suchwow-wallet-rpc \
-v $WALLET_PATH:/root \ -v $WALLET_PATH:/root \
-p 8888:8888 \ -p $WALLET_PORT:8888 \
lalanza808/wownero:latest \ lalanza808/wownero:latest \
wownero-wallet-rpc \ wownero-wallet-rpc \
--daemon-address $DAEMON_URI \ --daemon-address $DAEMON_URI \

@ -0,0 +1,223 @@
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}/suchwow_db.sqlite")
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 User(Model):
id = AutoField()
username = CharField()
address = CharField(null=True)
moderator = BooleanField(default=False)
banned = BooleanField(default=False)
ban_reason = TextField(null=True)
ban_timestamp = DateField(null=True)
login_timestamp = DateTimeField(null=True)
def get_wow_received(self):
tips = TipReceived.select().join(Post).where(Post.user == self)
return sum([tip.amount for tip in tips])
def get_wow_sent(self):
tips = TipSent.select().where(TipSent.from_user == self)
return sum([tip.amount + tip.fee for tip in tips])
def get_post_count(self):
posts = Post.select().where(Post.user == self)
return posts.count()
def get_post_addresses(self):
posts = Post.select().where(Post.user == self)
return [i.address_index for i in posts]
class Meta:
database = db
class Post(Model):
id = AutoField()
title = CharField()
text = CharField(null=True)
user = ForeignKeyField(User)
image_name = CharField()
account_index = IntegerField()
address_index = IntegerField(unique=True)
address = CharField(unique=True)
timestamp = DateTimeField(default=datetime.utcnow)
approved = BooleanField(default=False)
approved_by = ForeignKeyField(User, null=True)
def get_random(self):
all_posts = Post.select().where(Post.approved == True)
if all_posts:
return choice([i.id for i in all_posts])
else:
return None
def get_previous(self):
prev = Post.select().where(Post.id == self.id - 1).first()
if prev and prev.approved:
return prev.id
else:
return None
def get_next(self):
next = Post.select().where(Post.id == self.id + 1).first()
if next and next.approved:
return next.id
else:
return None
def get_image_path(self, thumbnail=False):
save_path_base = path.join(config.DATA_FOLDER, "uploads")
if thumbnail:
save_path = path.join(save_path_base, self.thumbnail)
else:
save_path = path.join(save_path_base, self.image_name)
return save_path
def save_thumbnail(self):
try:
image = Image.open(self.get_image_path())
image.thumbnail((200,200), Image.ANTIALIAS)
image.save(self.get_image_path(True), format=image.format, quality=90)
image.close()
return True
except:
return False
def strip_exif(self):
try:
image = Image.open(self.get_image_path())
data = image.getdata()
image_without_exif = Image.new(image.mode, image.size)
image_without_exif.putdata(data)
image_without_exif.save(self.get_image_path())
image_without_exif.close()
image.close()
except:
return False
def resize_image(self):
try:
with Image.open(self.get_image_path()) as img:
img.thumbnail((1800,1800))
img.save(self.get_image_path())
except:
return False
@property
def resized(self):
s = path.splitext(self.image_name)
return s[0] + '.resized' + s[1]
@property
def thumbnail(self):
s = path.splitext(self.image_name)
return s[0] + '.thumbnail' + s[1]
def get_wow_received(self):
tips = TipReceived.select().where(TipReceived.post == self)
return sum(tip.amount for tip in tips)
def hours_elapsed(self):
now = datetime.utcnow()
diff = now - self.timestamp
return diff.total_seconds() / 60 / 60
def show(self):
return {
'id': self.id,
'title': self.title,
'text': self.text,
'user': self.user.username,
'image_name': self.image_name,
'image_path': self.get_image_path(),
'thumbnail_name': self.thumbnail,
'thumbnail_path': self.get_image_path(True),
'account_index': self.account_index,
'address_index': self.address_index,
'address': self.address,
'timestamp': self.timestamp,
'approved': self.approved,
'approved_by': self.approved_by,
'received_wow': self.get_wow_received(),
'hours_elapsed': self.hours_elapsed(),
'user_tips_received': wownero.from_atomic(self.user.get_wow_received()),
'user_tips_sent': wownero.from_atomic(self.user.get_wow_sent())
}
class Meta:
database = db
class SocialPost(Model):
id = AutoField()
post = ForeignKeyField(Post)
service = CharField()
class Meta:
database = db
class Vote(Model):
id = AutoField()
post = ForeignKeyField(Post)
upvote = BooleanField()
timestamp = DateTimeField(default=datetime.now)
class Meta:
database = db
class AuditEvent(Model):
id = AutoField()
user = ForeignKeyField(User)
timestamp = DateTimeField(default=datetime.now)
action = CharField()
class Meta:
database = db
class TipReceived(Model):
id = AutoField()
post = ForeignKeyField(Post)
txid = CharField()
timestamp = DateTimeField()
amount = IntegerField()
fee = IntegerField()
class Meta:
database = db
class TipSent(Model):
id = AutoField()
from_user = ForeignKeyField(User)
to_user = ForeignKeyField(User)
txid = CharField()
timestamp = DateTimeField()
amount = IntegerField()
fee = IntegerField()
class Meta:
database = db

@ -1,12 +1,12 @@
from os import makedirs from os import makedirs, getenv
from random import choice
from datetime import datetime
import click import lorem, click
from flask import Blueprint, url_for, current_app from flask import Blueprint
from suchwow.models import Post, Profile, Comment, Notification, db, Moderator, Ban, AuditEvent from suchwow._models import db, User, Post, AuditEvent, TipSent, TipReceived, Vote
from suchwow.utils.helpers import get_latest_tipped_posts from suchwow.models import Post as OldPost
from suchwow.utils.helpers import get_top_posters, get_top_posts
from suchwow.reddit import make_post
from suchwow import wownero from suchwow import wownero
from suchwow import config from suchwow import config
@ -20,76 +20,203 @@ def init():
makedirs(f"{config.DATA_FOLDER}/{i}", exist_ok=True) makedirs(f"{config.DATA_FOLDER}/{i}", exist_ok=True)
# init db # init db
db.create_tables([Post, Profile, Comment, Notification, Moderator, Ban, AuditEvent]) db.create_tables([User, Post, AuditEvent, TipSent, TipReceived, Vote])
@bp.cli.command('generate_data')
@bp.cli.command("post_reddit") def generate_data():
@click.argument('last_hours') if getenv('FLASK_DEBUG', 0) == '1':
def post_reddit(last_hours): users = ['lza_menace', 'wowario', 'jwinterm', 'dsc', 'asymptotically']
posts = Post.select().where( for user in users:
Post.approved==True, moderator = False
Post.to_reddit==False if not User.select().where(User.username == user):
).order_by(Post.timestamp.asc()) if user == 'lza_menace':
for p in posts: moderator = True
if p.hours_elapsed() < int(last_hours): User.create(
if not p.to_reddit: username=user,
_p = make_post(p) moderator=moderator
if _p: )
p.to_reddit = True print(f'Created user {user}')
p.save()
return for i in range(1, 5):
wallet = wownero.Wallet()
address_idx, address = wallet.new_address(config.WALLET_ACCOUNT)
@bp.cli.command("create_accounts") wallet.store()
def create_accounts(): Post.create(
wallet = wownero.Wallet() title=lorem.sentence(),
for post in Post.select(): text=lorem.sentence(),
if post.account_index not in wallet.accounts(): user=choice(list(User.select())),
account = wallet.new_account() image_name='test.jpg',
print(f"Created account {account}") account_index=config.WALLET_ACCOUNT,
address_index=address_idx,
address=address
@bp.cli.command("payout_users") )
@bp.cli.command('process_tips')
def process_tips():
w = wownero.Wallet()
for user in User.select().order_by(User.id.desc()):
user_posts = Post.select().where(Post.user == user)
if user_posts.count() == 0:
continue
addresses = [i.address_index for i in user_posts]
txes = w.transfers(config.WALLET_ACCOUNT, addresses)
if txes:
for tx in txes['in']:
if tx['unlock_time'] > 0:
print('someone added a lock time to this tx. skipping for now.')
continue
if tx['confirmations'] < 5:
continue
_tx = TipReceived.select().where(TipReceived.txid == tx['txid']).first()
if not _tx:
post = Post.select().where(Post.address == tx['address']).first()
if not post:
print('No post exists with that address. Not sure wat do.')
else:
TipReceived.create(
post=post,
txid=tx['txid'],
timestamp=datetime.utcfromtimestamp(tx['timestamp']),
amount=sum(tx['amounts']),
fee=tx['fee']
)
print('Saved tip {} ({} WOW) received for post {} by {}'.format(
tx['txid'], wownero.from_atomic(sum(tx['amounts'])),
post.id, post.user.username
))
@bp.cli.command('payout_users')
def payout_users(): def payout_users():
wallet = wownero.Wallet() wallet = wownero.Wallet()
_fa = wownero.from_atomic balances = wallet.balances()
_aw = wownero.as_wownero print('Wallet balances are {} locked, {} unlocked'.format(
for post in Post.select(): wownero.from_atomic(balances[0]), wownero.from_atomic(balances[1])
try: ))
submitter = Profile.get(username=post.submitter) for user in User.select().join(Post, on=Post.user).distinct().order_by(User.id.asc()):
balances = wallet.balances(post.account_index) rcvd = user.get_wow_received()
url = url_for('post.read', id=post.id, _external=True) sent = user.get_wow_sent()
if balances[1] > 0.05: if rcvd == 0:
print(f"Post #{post.id} has {balances[1]} funds unlocked and ready to send. Sweeping all funds to user's address ({submitter.address}).") continue
sweep = wallet.sweep_all(account=post.account_index, dest_address=submitter.address) to_send = rcvd - sent
print(sweep) if to_send >= wownero.to_atomic(.5):
if "tx_hash_list" in sweep: print('{} has received {} atomic WOW but sent {} atomic WOW. Sending {} atomic WOW'.format(
amount = 0 user.username, wownero.from_atomic(rcvd),
for amt in sweep["amount_list"]: wownero.from_atomic(sent), wownero.from_atomic(to_send)
amount += int(amt) ))
except Exception as e: if balances[1] >= to_send:
print(f"Failed because: {e}") tx_data = {
'account_index': config.WALLET_ACCOUNT,
'destinations': [{
@bp.cli.command("show") 'address': user.address,
@click.argument("post_id") 'amount': to_send
def post_id(post_id): }],
'subaddress_indices': user.get_post_addresses(),
'priority': 0,
'unlock_time': 0,
'get_tx_key': True,
'do_not_relay': True,
'ring_size': 22
}
transfer = wallet.make_wallet_rpc('transfer', tx_data)
print(transfer)
if 'code' in transfer:
return
tx_data['destinations'][0]['amount'] = to_send - transfer['fee']
tx_data['do_not_relay'] = False
transfer = wallet.make_wallet_rpc('transfer', tx_data)
print(tx_data)
print(transfer)
if 'code' in transfer:
return
TipSent.create(
from_user=user,
to_user=user,
txid=transfer['tx_hash'],
timestamp=datetime.utcnow(),
amount=transfer['amount'],
fee=transfer['fee'],
)
print(f'Sent tip of {wownero.from_atomic(transfer["amount"])} WOW to {user} in tx_hash {transfer["tx_hash"]}')
wallet.make_wallet_rpc('store')
@bp.cli.command('fix_image')
@click.argument('post_id')
def fix_image(post_id):
p = Post.filter(id=post_id).first() p = Post.filter(id=post_id).first()
if p: if p:
print(p.show()) p.strip_exif()
p.resize_image()
else: else:
print("That post doesn't exist") print("That post doesn't exist")
@bp.cli.command('rescan')
def rescan():
wallet = wownero.Wallet()
wallet.make_wallet_rpc('rescan_blockchain')
@bp.cli.command('save')
def rescan():
wallet = wownero.Wallet()
wallet.make_wallet_rpc('store')
print('Saved wallet.')
@bp.cli.command("load_cache")
def load_cache(): @bp.cli.command("create_accounts")
current_app.logger.info('loading top posters into cache') def create_accounts():
get_top_posters() wallet = wownero.Wallet()
current_app.logger.info('done') highest_account = OldPost.select().order_by(OldPost.timestamp.desc()).first().account_index
current_app.logger.info('loading latest tipped into cache') print(f'Highest post account index is {highest_account} but highest wallet account is {wallet.accounts()[-1]}. Generating new accounts!')
get_latest_tipped_posts() while wallet.accounts()[-1] < highest_account:
current_app.logger.info('done') account = wallet.new_account()
for i in [1, 3, 7, 30, 9999]: print(f"Created account {account}")
current_app.logger.info(f'loading top posts last {i} days into cache') wallet.make_wallet_rpc('store')
get_top_posts(i)
current_app.logger.info('done')
# @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')

@ -21,6 +21,7 @@ WALLET_PROTO = getenv('WALLET_PROTO', 'http') #
WALLET_RPC_USER = getenv('WALLET_RPC_USER', 'suchwow') # WALLET_RPC_USER = getenv('WALLET_RPC_USER', 'suchwow') #
WALLET_RPC_PASS = getenv('WALLET_RPC_PASS', 'suchwow') # WALLET_RPC_PASS = getenv('WALLET_RPC_PASS', 'suchwow') #
WALLET_PASS = getenv('WALLET_PASS', 'zzzzzzz') # You specify all these wallet details in .env WALLET_PASS = getenv('WALLET_PASS', 'zzzzzzz') # You specify all these wallet details in .env
WALLET_ACCOUNT = getenv('WALLET_ACCOUNT', 0)
# Optional for posting to Reddit # Optional for posting to Reddit
PRAW_CLIENT_SECRET = getenv('PRAW_CLIENT_SECRET', None) PRAW_CLIENT_SECRET = getenv('PRAW_CLIENT_SECRET', None)

@ -2,6 +2,8 @@ from flask import Blueprint
from arrow import get as arrow_get from arrow import get as arrow_get
from suchwow.models import Moderator from suchwow.models import Moderator
from suchwow.wownero import from_atomic as _from_atomic
from suchwow.wownero import as_wownero
bp = Blueprint('filters', 'filters') bp = Blueprint('filters', 'filters')
@ -16,6 +18,10 @@ def shorten_address(a):
def humanize(d): def humanize(d):
return arrow_get(d).humanize() return arrow_get(d).humanize()
@bp.app_template_filter('from_atomic')
def from_atomic(a):
return as_wownero(_from_atomic(a))
@bp.app_template_filter('is_moderator') @bp.app_template_filter('is_moderator')
def is_moderator(s): def is_moderator(s):
m = Moderator.select().where(Moderator.username == s) m = Moderator.select().where(Moderator.username == s)

@ -1,7 +1,6 @@
from flask import jsonify, Blueprint, url_for, request, abort from flask import jsonify, Blueprint, url_for, request, abort
from suchwow.models import Post from suchwow._models import Post
from suchwow import wownero
bp = Blueprint("api", "api") bp = Blueprint("api", "api")
@ -22,18 +21,17 @@ def api_list():
limit = 30 limit = 30
all_posts = [] all_posts = []
posts = Post.select().where(Post.approved==True).order_by(Post.timestamp.desc()).limit(limit).offset(offset) posts = Post.select().where(
Post.approved == True
).order_by(
Post.timestamp.desc()
).limit(limit).offset(offset)
for post in posts: for post in posts:
wallet = wownero.Wallet()
if wallet.connected:
address = wallet.get_address(account=post.account_index)
else:
address = ''
payload = { payload = {
'image': url_for('post.uploaded_file', filename=post.image_name, _external=True), 'image': url_for('post.uploaded_file', filename=post.image_name, _external=True),
'submitter': post.submitter, 'submitter': post.user.username,
'address': address, 'address': post.address,
'title': post.title, 'title': post.title,
'text': post.text, 'text': post.text,
'href': url_for('post.read', id=post.id, _external=True), 'href': url_for('post.read', id=post.id, _external=True),

@ -1,17 +1,32 @@
from datetime import datetime, timedelta
import peewee
from flask import render_template, Blueprint, request from flask import render_template, Blueprint, request
from suchwow.utils.helpers import get_top_posters, get_top_posts from suchwow._models import Post, TipReceived, User
bp = Blueprint("leaderboard", "leaderboard") bp = Blueprint("leaderboard", "leaderboard")
@bp.route("/leaderboards/top_posters") @bp.route("/leaderboards/top_posters")
def top_posters(): def top_posters():
top_posters = get_top_posters() tips_received = peewee.fn.SUM(TipReceived.amount)
top_posters = User.select(
User, tips_received
).join(
Post, peewee.JOIN.LEFT_OUTER, on=Post.user
).join(
TipReceived, peewee.JOIN.LEFT_OUTER
).group_by(User.username).order_by(
tips_received.desc()
).limit(30)
return render_template("leaderboard.html", posters=top_posters) return render_template("leaderboard.html", posters=top_posters)
@bp.route("/leaderboards/top_posts") @bp.route("/leaderboards/top_posts")
def top_posts(): def top_posts():
tips_received = peewee.fn.SUM(TipReceived.amount)
days = request.args.get('days', 1) days = request.args.get('days', 1)
try: try:
days = int(days) days = int(days)
@ -20,6 +35,20 @@ def top_posts():
if days not in [1, 3, 7, 30, 9999]: if days not in [1, 3, 7, 30, 9999]:
days = 7 days = 7
posts = get_top_posts(days) new_date = datetime.utcnow() - timedelta(hours=(days * 24))
return render_template("post/top.html", posts=posts, days=days) posts = Post.select(Post, tips_received).join(
TipReceived
).where(
TipReceived.timestamp >= new_date
).group_by(
Post.id
).order_by(
tips_received.desc()
).limit(30)
return render_template(
"index.html",
posts=posts,
days=days,
title=f'Top Posts Last {days} Days'
)

@ -2,8 +2,8 @@ from math import ceil
from flask import Blueprint, request, render_template, flash from flask import Blueprint, request, render_template, flash
from suchwow.models import Post, Profile, Moderator from suchwow._models import Post, User, TipReceived
from suchwow.utils.helpers import get_latest_tipped_posts from suchwow.wownero import from_atomic
bp = Blueprint('main', 'main') bp = Blueprint('main', 'main')
@ -16,10 +16,10 @@ def index():
content = request.args.get("content", None) content = request.args.get("content", None)
if content == 'latest_tipped': if content == 'latest_tipped':
posts = get_latest_tipped_posts() posts = Post.select().join(TipReceived).distinct().order_by(TipReceived.timestamp.desc()).limit(30)
return render_template( return render_template(
"index.html", "index.html",
posts=posts[0:30], posts=posts,
title="Latest Tipped Memes" title="Latest Tipped Memes"
) )
@ -29,9 +29,13 @@ def index():
flash("Wow, wtf hackerman. Cool it.", "is-danger") flash("Wow, wtf hackerman. Cool it.", "is-danger")
page = 1 page = 1
posts = Post.select().where(Post.approved==True).order_by(Post.timestamp.desc()) posts = Post.select().where(Post.approved == True).order_by(Post.timestamp.desc())
if submitter: if submitter:
posts = posts.where(Post.submitter==submitter) user = User.select().where(User.username == submitter)
if not user:
flash('That user does not exist!', 'is-warning')
else:
posts = posts.where(Post.user == user)
paginated_posts = posts.paginate(page, itp) paginated_posts = posts.paginate(page, itp)
total_pages = ceil(posts.count() / itp) total_pages = ceil(posts.count() / itp)
@ -46,5 +50,11 @@ def index():
@bp.route("/about") @bp.route("/about")
def about(): def about():
mods = Profile.select().join(Moderator, on=(Profile.username == Moderator.username)) mods = User.select().where(User.moderator == True)
return render_template("about.html", mods=mods) return render_template(
"about.html",
mods=mods,
meme_count=Post.select().where(Post.approved == True).count(),
wow_received=round(from_atomic(sum([i.amount for i in TipReceived.select()])), 2),
memer_count=User.select().count(),
)

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

@ -8,10 +8,10 @@ from flask import render_template, Blueprint, request, flash
from flask import send_from_directory, redirect, url_for, current_app from flask import send_from_directory, redirect, url_for, current_app
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from suchwow import wownero from suchwow import wownero, config
from suchwow.models import Post, Profile, Comment, Ban from suchwow._models import User, Post, TipReceived
from suchwow.utils.decorators import login_required, profile_required, moderator_required from suchwow.utils.decorators import login_required, address_required, moderator_required
from suchwow.utils.helpers import allowed_file, is_moderator, get_session_user from suchwow.utils.helpers import allowed_file, get_session_user
from suchwow.utils.helpers import audit_event from suchwow.utils.helpers import audit_event
from suchwow.discord import post_discord_webhook from suchwow.discord import post_discord_webhook
@ -22,28 +22,21 @@ bp = Blueprint("post", "post")
def read(id): def read(id):
_address_qr = BytesIO() _address_qr = BytesIO()
qr_code = None qr_code = None
if Post.filter(id=id): post = Post.select().where(Post.id == id).first()
wallet = wownero.Wallet() if post:
post = Post.get(id=id)
if not post.approved: if not post.approved:
if not is_moderator(get_session_user()): if not User.select().where(User.username == get_session_user()).first().moderator:
flash("That post has not been approved.", "is-warning") flash("That post has not been approved.", "is-warning")
return redirect("/") return redirect("/")
if wallet.connected: qr_uri = f'wownero:{post.address}?tx_description=suchwow%20post%20{post.id}'
address = wallet.get_address(account=post.account_index) qrcode_make(qr_uri).save(_address_qr)
transfers = wallet.transfers(account=post.account_index) qr_code = b64encode(_address_qr.getvalue()).decode()
qr_uri = f'wownero:{address}?tx_description=suchwow%20post%20{post.id}' tips = TipReceived.select().where(TipReceived.post == post).order_by(TipReceived.timestamp.desc())
address_qr = qrcode_make(qr_uri).save(_address_qr)
qr_code = b64encode(_address_qr.getvalue()).decode()
else:
address = "?"
transfers = "?"
return render_template( return render_template(
"post/read.html", "post/read.html",
post=post, post=post,
address=address, qr_code=qr_code,
transfers=transfers, tips=tips
qr_code=qr_code
) )
else: else:
flash("No meme there, brah", "is-warning") flash("No meme there, brah", "is-warning")
@ -51,13 +44,11 @@ def read(id):
@bp.route("/post/create", methods=["GET", "POST"]) @bp.route("/post/create", methods=["GET", "POST"])
@login_required @login_required
@profile_required @address_required
def create(): def create():
submitter = get_session_user() u = User.select().where(User.username == get_session_user()).first()
u = Profile.filter(username=submitter) if u.banned:
banned = Ban.filter(user=u).first() flash(f"You can't post: {u.ban_reason}", "is-danger")
if banned:
flash(f"You can't post: {banned.reason}", "is-danger")
return redirect("/") return redirect("/")
if request.method == "POST": if request.method == "POST":
post_title = request.form.get("title") post_title = request.form.get("title")
@ -82,37 +73,34 @@ def create():
save_path_base = path.join(current_app.config["DATA_FOLDER"], "uploads") save_path_base = path.join(current_app.config["DATA_FOLDER"], "uploads")
save_path = path.join(save_path_base, filename) save_path = path.join(save_path_base, filename)
file.save(save_path) file.save(save_path)
try: wallet_found = False
while wallet_found is False:
wallet = wownero.Wallet() wallet = wownero.Wallet()
account_index = wallet.new_account() address_idx, address = wallet.new_address(config.WALLET_ACCOUNT)
in_use = Post.select().where(Post.account_index == account_index).first() in_use = Post.select().where(Post.address == address).first()
if in_use: if in_use:
flash("Suchwow wallet is fucked up! Try again later.", "is-danger") continue
return redirect(request.url) wallet_found = True
except: post = Post(
flash("Suchwow wallet is fucked up! Try again later.", "is-danger") title=post_title,
return redirect(request.url) text=request.form.get("text", ""),
post = Post( user=u,
title=post_title, image_name=filename,
text=request.form.get("text", ""), account_index=config.WALLET_ACCOUNT,
submitter=submitter, address_index=address_idx,
image_name=filename, address=address
account_index=account_index, )
address_index=0 post.save()
) post.save_thumbnail()
post.save() audit_event(f'Created new post {post.id}')
post.save_thumbnail() flash("New post created and pending approval!", "is-success")
url = url_for('post.read', id=post.id, _external=True) return redirect(url_for("main.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") return render_template("post/create.html")
@bp.route("/post/<id>/approve") @bp.route("/post/<id>/approve")
@moderator_required @moderator_required
def approve(id): def approve(id):
post = Post.get(id=id) post = Post.select().where(Post.id == id).first()
url = url_for('post.read', id=post.id, _external=True)
if post: if post:
if not post.approved: if not post.approved:
post.approved = True post.approved = True
@ -129,12 +117,10 @@ def approve(id):
@bp.route("/post/<id>/delete") @bp.route("/post/<id>/delete")
@login_required @login_required
def delete(id): def delete(id):
filtered = Post.filter(id=id) post = Post.select().where(Post.id == id).first()
user = get_session_user() user = User.select().where(User.username == get_session_user()).first()
is_mod = is_moderator(user) if post:
if filtered: if user == post.user or user.moderator:
post = filtered.first()
if user == post.submitter or is_mod:
save_path_base = path.join(current_app.config["DATA_FOLDER"], "uploads") save_path_base = path.join(current_app.config["DATA_FOLDER"], "uploads")
save_path = path.join(save_path_base, post.image_name) save_path = path.join(save_path_base, post.image_name)
try: try:
@ -144,7 +130,7 @@ def delete(id):
audit_event(f'Deleted post {post.id}') audit_event(f'Deleted post {post.id}')
post.delete_instance() post.delete_instance()
flash("Deleted that shit, brah!", "is-success") flash("Deleted that shit, brah!", "is-success")
if is_mod: if user.moderator:
return redirect(url_for("mod.pending_posts")) return redirect(url_for("mod.pending_posts"))
else: else:
return redirect(url_for("main.index")) return redirect(url_for("main.index"))

@ -1,7 +1,8 @@
from flask import render_template, Blueprint, flash from flask import render_template, Blueprint, flash, request, redirect, flash
from flask import request, redirect, url_for, session
from suchwow.models import Profile from suchwow._models import User
from suchwow.utils.decorators import login_required from suchwow.utils.decorators import login_required
from suchwow.utils.helpers import get_session_user
bp = Blueprint("profile", "profile") bp = Blueprint("profile", "profile")
@ -9,27 +10,18 @@ bp = Blueprint("profile", "profile")
@bp.route("/profile/edit", methods=["GET", "POST"]) @bp.route("/profile/edit", methods=["GET", "POST"])
@login_required @login_required
def edit(): def edit():
un = session["auth"]["preferred_username"] user = User.select().where(User.username == get_session_user()).first()
profile_exists = Profile.filter(username=un)
if request.method == "POST": if request.method == "POST":
address = request.form.get("address") address = request.form.get("address")
if len(address) in [97, 108]: if len(address) in [97, 108]:
if profile_exists: if user:
profile = Profile.get(username=un) user.address = address
profile.address = address user.save()
profile.save()
else: else:
profile = Profile( User.create(username=get_session_user())
username=un, flash('Wallet address saved!', 'is-success')
address=address
)
profile.save()
return redirect(request.args.get("redirect", "/")) return redirect(request.args.get("redirect", "/"))
else: else:
flash("WTF bro, that's not a valid Wownero address", "is-warning") flash("WTF bro, that's not a valid Wownero address", "is-warning")
return redirect(request.url) return redirect(request.url)
if profile_exists: return render_template("profile/edit.html", profile=user)
profile = Profile.get(username=un)
else:
profile = None
return render_template("profile/edit.html", profile=profile)

File diff suppressed because one or more lines are too long

@ -2,6 +2,27 @@
{% block content %} {% block content %}
<nav class="level" style="margin: 3em 0">
<div class="level-item has-text-centered">
<div>
<p class="heading">Memes</p>
<p class="title">{{ meme_count }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Tips</p>
<p class="title">{{ wow_received }} WOW</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Memers</p>
<p class="title">{{ memer_count }}</p>
</div>
</div>
</nav>
<div class="container" style="text-align:center;"> <div class="container" style="text-align:center;">
<div class="about content"> <div class="about content">
<h1>About</h1> <h1>About</h1>

@ -47,6 +47,7 @@
<meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-TileColor" content="#da532c">
<meta name="keywords" content="wownero, wow, memes, monero, xmr, cryptocurrency"> <meta name="keywords" content="wownero, wow, memes, monero, xmr, cryptocurrency">
<link href="/static/css/bulma.min.css" rel="stylesheet"> <link href="/static/css/bulma.min.css" rel="stylesheet">
<link href="/static/css/font-awesome.all.min.css" rel="stylesheet">
<link href="/static/css/custom.css" rel="stylesheet"> <link href="/static/css/custom.css" rel="stylesheet">
<title>SuchWow!</title> <title>SuchWow!</title>
</head> </head>

@ -16,19 +16,18 @@
{% if posts %} {% if posts %}
{% for row in posts | batch(4) %} {% for row in posts | batch(4) %}
<div class="columns"> <div class="columns">
{% for p in row %} {% for post in row %}
{% set post = p.show() %}
<div class="column"> <div class="column">
<div class="card"> <div class="card">
<div class="card-image"> <div class="card-image">
<a href="{{ url_for('post.read', id=post.id) }}"> <a href="{{ url_for('post.read', id=post.id) }}">
{% if p.get_image_path().endswith('mp4') %} {% if post.get_image_path().endswith('mp4') %}
<video style="max-height: 100vh!important;" controls> <video style="max-height: 100vh!important;" controls>
<source src="{{ url_for('post.uploaded_file', filename=p.image_name) }}" type="video/mp4"> <source src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" type="video/mp4">
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
{% else %} {% else %}
<img alt="SuchWow #{{ post.id }} - {{ post.title }} by {{ post.submitter }}" src="{{ url_for('post.uploaded_file', filename=post.thumbnail_name) }}" /> <img alt="SuchWow #{{ post.id }} - {{ post.title }} by {{ post.user.username }}" src="{{ url_for('post.uploaded_file', filename=post.thumbnail) }}" />
{% endif %} {% endif %}
</a> </a>
</div> </div>
@ -38,13 +37,12 @@
<p class="title is-4"> <p class="title is-4">
<a href="{{ url_for('post.read', id=post.id) }}">{{ post.title }}</a> <a href="{{ url_for('post.read', id=post.id) }}">{{ post.title }}</a>
</p> </p>
<p class="subtitle is-6"><a href="/?submitter={{ post.submitter }}">{{ post.submitter }}</a></p> <p class="subtitle is-6"><a href="/?submitter={{ post.user.username }}">{{ post.user.username }}</a></p>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
{{ post.text | truncate(60) }} {{ post.text | truncate(60) }}
<p><strong>{{ post.received_wow }} WOW received</strong></p> <p><strong>{{ post.get_wow_received() | from_atomic }} WOW received</strong></p>
<time datetime="2016-1-1">{{ post.timestamp.year }}-{{ post.timestamp.month }}-{{ post.timestamp.day }} {{ post.timestamp.hour }}:{{ post.timestamp.minute }} UTC</time> <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> <p>({{ post.timestamp | humanize }})</p>
</div> </div>

@ -13,11 +13,11 @@
<th>Post Count</th> <th>Post Count</th>
<th>Amount</th> <th>Amount</th>
</tr> </tr>
{% for poster, data in posters.items() | sort(attribute='1.amount', reverse=True) %} {% for poster in posters %}
<tr> <tr>
<td><a href="/?submitter={{ poster }}">{{ poster }}</a></td> <td><a href="/?submitter={{ poster.username }}">{{ poster.username }}</a></td>
<td>{{ data["posts"] | length }}</td> <td>{{ poster.get_post_count() }}</td>
<td>{{ data["amount"] }} WOW</td> <td>{{ poster.get_wow_received() | from_atomic }} WOW</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

@ -8,11 +8,11 @@
{% for ban in bans %} {% for ban in bans %}
<article class="message" style="width: 30%; margin: 2em auto;"> <article class="message" style="width: 30%; margin: 2em auto;">
<div class="message-header"> <div class="message-header">
<p>{{ ban.user.username }}</p> <p>{{ ban.username }}</p>
<a href="?delete={{ ban.user.username }}" class="delete"></a> <a href="?delete={{ ban.username }}" class="delete"></a>
</div> </div>
<div class="message-body"> <div class="message-body">
{{ ban.reason }} {{ ban.ban_reason }}
</div> </div>
</article> </article>
{% endfor %} {% endfor %}

@ -12,11 +12,30 @@
<li class="is-active"><a href="#" aria-current="page">Post {{ post.id }}</a></li> <li class="is-active"><a href="#" aria-current="page">Post {{ post.id }}</a></li>
</ul> </ul>
</nav> </nav>
{% if post.hidden %} <div class="tabs is-fullwidth">
<h2>You cannot see this post</h2> <ul>
{% else %} <li>
<a {% if post.get_previous() %}href="{{ url_for('post.read', id=post.get_previous()) }}"{% else %}class="disabled"{% endif %}>
<span class="icon"><i class="fas fa-angle-left" aria-hidden="true"></i></span>
<span>Previous</span>
</a>
</li>
<li>
<a {% if post.get_random() %}href="{{ url_for('post.read', id=post.get_random()) }}{% endif %}">
<span class="icon"><i class="fas fa-question" aria-hidden="true"></i></span>
<span>Random</span>
</a>
</li>
<li>
<a {% if post.get_next() %}href="{{ url_for('post.read', id=post.get_next()) }}"{% else %}class="disabled"{% endif %}>
<span>Next</span>
<span class="icon"><i class="fas fa-angle-right" aria-hidden="true"></i></span>
</a>
</li>
</ul>
</div>
<!-- Post Info --> <!-- Post Info -->
<section class="section"> <section class="section" style="padding: 0;">
<div class="content"> <div class="content">
<h1>{{ post.title }}</h1> <h1>{{ post.title }}</h1>
<p>{{ post.text }}</p> <p>{{ post.text }}</p>
@ -24,68 +43,49 @@
<a href="{{ url_for('post.approve', id=post.id) }}" class="button is-success">Approve</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> <a href="{{ url_for('post.delete', id=post.id) }}" class="button is-danger">Reject</a>
{% endif %} {% 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> <p class="mt-2">Submitted by <i><u><a href="/?submitter={{ post.user.username }}">{{ post.user.username }}</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 style="max-width: 80vh; margin: 0 auto;">
{% if post.get_image_path().endswith('mp4') %} {% if post.get_image_path().endswith('mp4') %}
<video style="max-height: 100vh!important;" controls> <video style="max-height: 100vh!important;" controls>
<source src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" type="video/mp4"> <source src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" type="video/mp4">
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
{% else %} {% else %}
<img alt="SuchWow #{{ post.id }} - {{ post.title }} by {{ post.submitter }}" src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" /> <a href="{{ url_for('post.uploaded_file', filename=post.image_name, _external=True) }}">
<img alt="SuchWow #{{ post.id }} - {{ post.title }} by {{ post.user.username }}" src="{{ url_for('post.uploaded_file', filename=post.image_name) }}" />
</a>
{% endif %} {% endif %}
</div>
</div> </div>
</section> </section>
<!-- Payments --> <!-- Payments -->
<section class="section content"> <section class="section content">
<h3>Payments</h3> <h3>Payments</h3>
<p style="word-break:break-all;">Vote for this post by sending WOW to the following address:<br><i>{{ address }}</i></p> <p style="word-break:break-all;">Vote for this post by sending WOW to the following address:<br><i>{{ post.address }}</i></p>
{% if qr_code %} {% if qr_code %}
<img src="data:image/png;base64,{{ qr_code }}" width=180 class="center"><br /><br /> <img src="data:image/png;base64,{{ qr_code }}" width=180 class="center"><br /><br />
{% endif %} {% endif %}
<div class="columns"> <div class="columns">
<div class="column content"> <div class="column content">
<h4>WOW Received</h4> <h4>{{ tips | sum(attribute='amount') | from_atomic }} WOW Received</h4>
{% if transfers.in %} {% if tips %}
<ul> <ul>
{% for transfer in transfers.in %} {% for tip in tips %}
{% if transfer.amount > 0 %} <li class="is-small" style="font-size:.8em;">
<li> {{ tip.amount | from_atomic }} WOW
{{ transfer.amount / 100000000000 }} WOW (<a href="https://explore.wownero.com/tx/{{ tip.txid }}" target="_blank">{{ tip.txid | shorten_address }}</a>)
(<a href="https://wownero.club/transaction/{{ transfer.txid }}" target="_blank">{{ transfer.txid | shorten_address }}</a>) - {{ tip.timestamp | humanize }}
- {{ transfer.timestamp | humanize }}
</li> </li>
{% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
<p>No WOW received yet. Show this post some love!</p> <p>No WOW received yet. Show this post some love!</p>
{% endif %} {% endif %}
</div> </div>
<div class="column content">
<h4>WOW Sent</h4>
{% if transfers.out %}
<ul>
{% for transfer in transfers.out %}
<li>
{{ transfer.amount / 100000000000 }} WOW
(<a href="https://wownero.club/transaction/{{ transfer.txid }}" target="_blank">{{ transfer.txid | shorten_address }}</a>)
- {{ transfer.timestamp | humanize }}
</li>
{% endfor %}
</ul>
{% else %}
<p>No payouts yet.</p>
{% endif %}
</div>
</div> </div>
</section> </section>
{% endif %}
{% if config.DEBUG %} {% if config.DEBUG %}
{{ post.show() }} {{ post.show() }}
{% endif %} {% endif %}

@ -1,62 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="container" style="text-align:center;">
<h1 class="title">Top Memes Last {{ days }} Days</h1>
<section class="section">
{% 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">
<div class="card-image">
<a href="{{ url_for('post.read', id=post.id) }}">
<img src="{{ url_for('post.uploaded_file', filename=post.thumbnail_name) }}" alt="Placeholder image">
</a>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<p class="title is-4">
<a href="{{ url_for('post.read', id=post.id) }}">{{ post.title }}</a>
</p>
<p class="subtitle is-6"><a href="/?submitter={{ post.submitter }}">{{ post.submitter }}</a></p>
</div>
</div>
<div class="content">
{{ post.text | truncate(60) }}
<p><strong>{{ post.received_wow }} WOW received</strong></p>
<time datetime="2016-1-1">{{ post.timestamp.year }}-{{ post.timestamp.month }}-{{ post.timestamp.day }} {{ post.timestamp.hour }}:{{ post.timestamp.minute }} UTC</time>
</div>
</div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endfor %}
{% else %}
<p>No posts yet!</p>
{% endif %}
</section>
{% if total_pages %}
<nav class="pagination is-centered pb-4" role="navigation" aria-label="pagination">
<ul class="pagination-list">
{% for p in range(1, total_pages + 1) %}
<a href="{% if request.args.submitter %}/?submitter={{ request.args.submitter }}&{% else %}/?{% endif %}page={{ p }}" class="pagination-link {% if p == page %}current-page-btn{% endif %}">{{ p }}</a>
{% endfor %}
</ul>
</nav>
{% endif %}
</div>
{% endblock %}

@ -6,17 +6,21 @@
<div class="edit"> <div class="edit">
<h1>Edit Profile</h1> <h1>Edit Profile</h1>
<p>You need to setup your profile before you can submit memes. As of now this only consists of a payout address so we know where to send Wownero if someone sends funds for your post.</p> <p>You need to setup your profile before you can submit memes. As of now this only consists of a payout address so we know where to send Wownero if someone sends funds for your post.</p>
<form method=post enctype=multipart/form-data class="form-horizontal"> <form method=post enctype=multipart/form-data class="mt-4">
<div class="form-group"> <div class="field">
<label class="sr-only" for="address">Payout Address</label> <label class="label">Payout Address</label>
<input type="text" class="form-control mb-2 mr-sm-2 mb-sm-0" id="address" placeholder="Wownero address for payouts" name="address"> <div class="control">
<input class="input" type="text" placeholder="Wownero address for payouts" name="address">
</div>
</div> </div>
<div class="form-group"> <div class="field is-grouped">
<button type="submit" class="btn btn-success">Submit</button> <div class="control">
<button class="button is-link">Submit</button>
</div>
</div> </div>
</form> </form>
{% if profile %} {% if profile %}
<p style="word-break:break-all;">Existing Address:<br>{{ profile.address }}</p> <p style="word-break:break-all;" class="mt-4">Existing Address:<br>{{ profile.address }}</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>

@ -1,6 +1,6 @@
from flask import session, redirect, url_for, flash from flask import session, redirect, url_for, flash
from functools import wraps from functools import wraps
from suchwow.models import Profile, Moderator from suchwow._models import User
def login_required(f): def login_required(f):
@ -16,19 +16,19 @@ def moderator_required(f):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if "auth" not in session or not session["auth"]: if "auth" not in session or not session["auth"]:
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
m = Moderator.filter(username=session["auth"]["preferred_username"]) u = User.select().where(User.username == session["auth"]["preferred_username"]).first()
if m: if u.moderator:
return f(*args, **kwargs) return f(*args, **kwargs)
else: else:
flash("You are not a moderator", "is-warning") flash("You are not a moderator", "is-warning")
return redirect(url_for("main.index")) return redirect(url_for("main.index"))
return decorated_function return decorated_function
def profile_required(f): def address_required(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
un = session["auth"]["preferred_username"] u = User.select().where(User.username == session["auth"]["preferred_username"]).first()
if not Profile.filter(username=un): if not u.address:
url = "{}?redirect={}".format( url = "{}?redirect={}".format(
url_for("profile.edit"), url_for("profile.edit"),
url_for("post.create") url_for("post.create")

@ -4,8 +4,7 @@ from datetime import datetime, timedelta
from flask import session from flask import session
from suchwow.models import Moderator, Post, AuditEvent, Profile from suchwow._models import AuditEvent, User
from suchwow.wownero import Wallet, from_atomic
from suchwow import config from suchwow import config
@ -13,19 +12,12 @@ def allowed_file(filename):
return "." in filename and \ return "." in filename and \
filename.rsplit(".", 1)[1].lower() in config.ALLOWED_EXTENSIONS filename.rsplit(".", 1)[1].lower() in config.ALLOWED_EXTENSIONS
def is_moderator(username): def get_current_user():
m = Moderator.filter(username=username) u = User.select().where(User.username == get_session_user()).first()
if m: return u
return True
else:
return False
def get_profile():
p = Profile.filter(username=get_session_user()).first()
return p
def audit_event(event): def audit_event(event):
e = AuditEvent(user=get_profile(), action=event) e = AuditEvent(user=get_current_user(), action=event)
e.save() e.save()
def get_session_user(): def get_session_user():
@ -33,116 +25,3 @@ def get_session_user():
return None return None
return session["auth"]["preferred_username"].strip() return session["auth"]["preferred_username"].strip()
def get_latest_tipped_posts():
key_name = 'latest_tips'
posts = []
tipped_posts = rw_cache(key_name)
if not tipped_posts:
w = Wallet()
data = {}
for acc in w.accounts():
txes = w.transfers(acc)
if 'in' in txes:
for tx in txes['in']:
p = Post.select().where(
Post.account_index==acc
).first()
if p:
data[tx['timestamp']] = p
dates = sorted(data, reverse=True)
for d in dates:
if not data[d] in posts:
posts.append(data[d])
tipped_posts = rw_cache(key_name, posts)
return tipped_posts
def get_top_posters():
top_posters = {}
posts = rw_cache('top_posters')
if not posts:
posts = Post.select().where(Post.approved==True)
for post in posts:
transfers = []
incoming = Wallet().incoming_transfers(post.account_index)
if "transfers" in incoming:
for xfer in incoming["transfers"]:
transfers.append(from_atomic(xfer["amount"]))
total = sum(transfers)
if post.submitter not in top_posters:
top_posters[post.submitter] = {"amount": 0, "posts": []}
top_posters[post.submitter]["amount"] += float(total)
top_posters[post.submitter]["posts"].append(post)
rw_cache('top_posters', top_posters)
else:
top_posters = posts
return top_posters
def get_top_posts(days=1):
top_posts = []
try:
days = int(days)
except:
days = 1
# 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)}'
posts = rw_cache(key_name)
if not posts:
posts = Post.select().where(
Post.approved==True,
Post.timestamp > diff
).order_by(
Post.timestamp.desc()
)
for post in posts:
p = post.show()
if isinstance(p['received_wow'], float):
top_posts.append(p)
posts = rw_cache(key_name, top_posts)
return posts
# Use hacky filesystem cache since i dont feel like shipping redis
def rw_cache(key_name, data=None, diff_seconds=3600):
pickle_file = path.join(config.DATA_FOLDER, f'{key_name}.pkl')
try:
if path.isfile(pickle_file):
mtime_ts = path.getmtime(pickle_file)
mtime = datetime.fromtimestamp(mtime_ts)
now = datetime.now()
diff = now - mtime
# If pickled data file is less than an hour old, load it and render page
# Otherwise, determine balances, build json, store pickled data, and render page
if diff.seconds < diff_seconds:
print(f'unpickling {key_name}')
with open(pickle_file, 'rb') as f:
pickled_data = pickle.load(f)
return pickled_data
else:
if data:
print(f'pickling {key_name}')
with open(pickle_file, 'wb') as f:
f.write(pickle.dumps(data))
return data
else:
return None
else:
if data:
print(f'pickling {key_name}')
with open(pickle_file, 'wb') as f:
f.write(pickle.dumps(data))
return data
else:
return None
except:
return None

@ -47,22 +47,12 @@ class Wallet(object):
def height(self): def height(self):
return self.make_wallet_rpc('get_height', {}) return self.make_wallet_rpc('get_height', {})
def spend_key(self):
return self.make_wallet_rpc('query_key', {'key_type': 'spend_key'})['key']
def view_key(self):
return self.make_wallet_rpc('query_key', {'key_type': 'view_key'})['key']
def seed(self):
return self.make_wallet_rpc('query_key', {'key_type': 'mnemonic'})['key']
def accounts(self): def accounts(self):
_accounts = self.make_wallet_rpc('get_accounts') _accounts = self.make_wallet_rpc('get_accounts')
return [i['account_index'] for i in _accounts['subaddress_accounts']] return [i['account_index'] for i in _accounts['subaddress_accounts']]
def new_account(self, label=None): def new_account(self, label=None):
_account = self.make_wallet_rpc('create_account', {'label': label}) _account = self.make_wallet_rpc('create_account', {'label': label})
self.store()
return _account['account_index'] return _account['account_index']
def addresses(self, account, addr_indices=None): def addresses(self, account, addr_indices=None):
@ -77,34 +67,33 @@ class Wallet(object):
addresses[_addr['address_index']] = _addr['address'] addresses[_addr['address_index']] = _addr['address']
return addresses return addresses
def get_address(self, account): def get_address(self, account, address_indices=[]):
qdata = {'account_index': account} qdata = {'account_index': account, 'address_index': address_indices}
_addresses = self.make_wallet_rpc('get_address', qdata) _addresses = self.make_wallet_rpc('get_address', qdata)
if 'address' in _addresses: return _addresses
return _addresses['address']
else:
return None
def new_address(self, account, label=None): def new_address(self, account, label=None):
data = {'account_index': account, 'label': label} data = {'account_index': account, 'label': label}
_address = self.make_wallet_rpc('create_address', data) _address = self.make_wallet_rpc('create_address', data)
self.store()
return (_address['address_index'], _address['address']) return (_address['address_index'], _address['address'])
def transfers(self, account, address_indices=[]): def transfers(self, account_index=0, address_indices=[], _in=True, _out=True):
data = { data = {
'account_index': account, 'account_index': account_index,
'subaddr_indices': address_indices, 'subaddr_indices': address_indices,
'in': True, 'in': _in,
'out': True 'out': _out
} }
_transfers = self.make_wallet_rpc('get_transfers', data) _transfers = self.make_wallet_rpc('get_transfers', data)
return _transfers return _transfers
def balances(self, account): def balances(self, address_indices=[]):
data = {'account_index': account} data = {
'account_index': config.WALLET_ACCOUNT,
'address_indices': address_indices
}
_balance = self.make_wallet_rpc('get_balance', data) _balance = self.make_wallet_rpc('get_balance', data)
return (from_atomic(_balance['balance']), from_atomic(_balance['unlocked_balance'])) return (_balance['balance'], _balance['unlocked_balance'])
def transfer(self, dest_address, amount, priority, account): def transfer(self, dest_address, amount, priority, account):
data = { data = {
@ -122,26 +111,15 @@ class Wallet(object):
self.store() self.store()
return transfer return transfer
def sweep_all(self, account, dest_address): def incoming_transfers(self, transfer_type='all'):
data = {
'address': dest_address,
'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):
data = { data = {
'transfer_type': transfer_type, 'transfer_type': transfer_type,
'account_index': account, 'account_index': config.WALLET_ACCOUNT
'verbose': verbose
} }
transfers = self.make_wallet_rpc('incoming_transfers', data) transfers = self.make_wallet_rpc('incoming_transfers', data)
return transfers return transfers
def to_atomic(amount): def to_atomic(amount):
if not isinstance(amount, (Decimal, float) + six.integer_types): if not isinstance(amount, (Decimal, float) + six.integer_types):
raise ValueError("Amount '{}' doesn't have numeric type. Only Decimal, int, long and " raise ValueError("Amount '{}' doesn't have numeric type. Only Decimal, int, long and "

@ -0,0 +1,7 @@
#!/bin/bash
rsync -avzP australia:/opt/suchwow/data/uploads/ data/uploads/
rsync -avzP australia:/opt/suchwow/data/sqlite.db data/sqlite.db
rsync -avzP australia:/opt/suchwow/data/migrate_data.pkl data/migrate_data.pkl
.venv/bin/python3 import.py
Loading…
Cancel
Save