2024-02-19 15:18:49 +11:00
|
|
|
from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory
|
|
|
|
import os
|
|
|
|
import dotenv
|
|
|
|
import requests
|
|
|
|
import datetime
|
|
|
|
import json
|
|
|
|
import threading
|
|
|
|
import nacl.signing
|
|
|
|
import nacl.encoding
|
|
|
|
import nacl.exceptions
|
|
|
|
import base58
|
|
|
|
import render
|
2024-02-26 19:37:03 +11:00
|
|
|
import hashlib
|
|
|
|
import random
|
2024-02-19 15:18:49 +11:00
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
dotenv.load_dotenv()
|
|
|
|
|
2024-02-26 19:37:03 +11:00
|
|
|
|
2024-02-19 15:30:48 +11:00
|
|
|
DISCORD_WEBHOOK = os.getenv('DISCORD_WEBHOOK')
|
2024-02-26 23:32:34 +11:00
|
|
|
utc_now = datetime.datetime.utcnow()
|
2024-02-19 15:30:48 +11:00
|
|
|
|
2024-02-19 15:18:49 +11:00
|
|
|
# If votes file doesn't exist, create it
|
|
|
|
if not os.path.isfile('data/votes.json'):
|
|
|
|
with open('data/votes.json', 'w') as file:
|
|
|
|
json.dump([], file)
|
|
|
|
|
|
|
|
|
|
|
|
#Assets routes
|
|
|
|
@app.route('/assets/<path:path>')
|
|
|
|
def send_report(path):
|
|
|
|
return send_from_directory('templates/assets', path)
|
|
|
|
|
|
|
|
@app.route('/assets/js/bundle.js')
|
|
|
|
def send_bundle():
|
|
|
|
return send_from_directory('dist', 'bundle.js')
|
|
|
|
|
|
|
|
@app.route('/sitemap')
|
|
|
|
@app.route('/sitemap.xml')
|
|
|
|
def sitemap():
|
|
|
|
# Remove all .html from sitemap
|
|
|
|
with open('templates/sitemap.xml') as file:
|
|
|
|
sitemap = file.read()
|
|
|
|
|
|
|
|
sitemap = sitemap.replace('.html', '')
|
|
|
|
return make_response(sitemap, 200, {'Content-Type': 'application/xml'})
|
|
|
|
|
|
|
|
@app.route('/favicon.png')
|
|
|
|
def faviconPNG():
|
|
|
|
return send_from_directory('templates/assets/img', 'favicon.png')
|
|
|
|
|
|
|
|
|
|
|
|
# Main routes
|
|
|
|
@app.route('/')
|
|
|
|
def index():
|
|
|
|
year = datetime.datetime.now().year
|
2024-02-26 19:37:03 +11:00
|
|
|
|
|
|
|
|
|
|
|
info = get_vote_info()
|
|
|
|
options = render.options(info["options"])
|
2024-02-25 21:39:04 +11:00
|
|
|
|
2024-02-26 19:37:03 +11:00
|
|
|
enabled = info["enabled"]
|
|
|
|
end = datetime.datetime.strptime(info["end"], "%Y-%m-%d")
|
2024-02-26 23:32:34 +11:00
|
|
|
smallEnd = end.strftime("%Y-%m-%d")
|
|
|
|
# Format as 2024-02-27T00:00:00Z
|
|
|
|
unixEnd = end.strftime("%Y-%m-%dT00:00:00Z")
|
|
|
|
|
|
|
|
if not hasStarted() or hasEnded():
|
|
|
|
enabled = False
|
2024-02-26 19:37:03 +11:00
|
|
|
|
|
|
|
revote = "not" if not info["revote"] else ""
|
|
|
|
if info["public"]:
|
|
|
|
votes = render.votes()
|
|
|
|
else:
|
|
|
|
votes = ""
|
|
|
|
|
|
|
|
return render_template('index.html',year=year,votes=votes, options=options,
|
2024-02-26 23:32:34 +11:00
|
|
|
current_vote=info["vote"], description=info["description"],
|
|
|
|
end=end,enabled=enabled, public=info["public"],
|
|
|
|
revote=revote, smallEnd=smallEnd,
|
|
|
|
unixEnd=unixEnd,ended=hasEnded(),
|
|
|
|
notStarted=not hasStarted(),starts=startTime().strftime("%Y-%m-%d") + " UTC")
|
2024-02-19 15:18:49 +11:00
|
|
|
|
|
|
|
@app.route('/<path:path>')
|
|
|
|
def catch_all(path):
|
|
|
|
year = datetime.datetime.now().year
|
|
|
|
# If file exists, load it
|
|
|
|
if os.path.isfile('templates/' + path):
|
|
|
|
return render_template(path, year=year)
|
|
|
|
|
|
|
|
# Try with .html
|
|
|
|
if os.path.isfile('templates/' + path + '.html'):
|
|
|
|
return render_template(path + '.html', year=year)
|
|
|
|
|
2024-02-19 15:28:51 +11:00
|
|
|
return render_template('404.html', year=year), 404
|
2024-02-19 15:18:49 +11:00
|
|
|
|
|
|
|
@app.route('/vote')
|
|
|
|
def vote():
|
|
|
|
print('Voting')
|
|
|
|
# Get args
|
|
|
|
args = request.args
|
|
|
|
# Convert to json
|
|
|
|
data = args.to_dict()
|
|
|
|
|
|
|
|
print(data)
|
|
|
|
|
|
|
|
# Verify signature
|
|
|
|
message = data["message"]
|
|
|
|
signature = data["signature"]
|
|
|
|
public_key = data["walletAddress"]
|
|
|
|
percent = data["percent"]
|
|
|
|
|
2024-02-26 19:37:03 +11:00
|
|
|
# Check if revote is enabled
|
|
|
|
info = get_vote_info()
|
|
|
|
if not info['revote']:
|
|
|
|
with open('data/votes.json') as file:
|
|
|
|
votes = json.load(file)
|
|
|
|
for vote in votes:
|
|
|
|
if vote["walletAddress"] == public_key:
|
|
|
|
return render_template('revotes.html', year=datetime.datetime.now().year)
|
|
|
|
|
|
|
|
# Make sure the voting is enabled and hasn't ended
|
|
|
|
if not info['enabled']:
|
|
|
|
return render_template('404.html', year=datetime.datetime.now().year)
|
2024-02-26 23:32:34 +11:00
|
|
|
|
|
|
|
if hasEnded():
|
2024-02-26 19:37:03 +11:00
|
|
|
return render_template('404.html', year=datetime.datetime.now().year)
|
|
|
|
|
2024-02-26 23:32:34 +11:00
|
|
|
if not hasStarted():
|
|
|
|
return render_template('404.html', year=datetime.datetime.now().year)
|
2024-02-26 19:37:03 +11:00
|
|
|
|
2024-02-19 15:18:49 +11:00
|
|
|
# Verify signature
|
|
|
|
try:
|
|
|
|
# Decode base58 encoded strings
|
|
|
|
public_key_bytes = base58.b58decode(public_key)
|
|
|
|
signature_bytes = base58.b58decode(signature)
|
|
|
|
message_bytes = message.encode('utf-8')
|
|
|
|
|
|
|
|
# Verify the signature
|
|
|
|
verify_key = nacl.signing.VerifyKey(public_key_bytes)
|
|
|
|
verify_key.verify(message_bytes, signature_bytes)
|
|
|
|
|
|
|
|
# Signature is valid
|
|
|
|
data["verified"] = True
|
|
|
|
|
|
|
|
except (nacl.exceptions.BadSignatureError, nacl.exceptions.CryptoError) as e:
|
|
|
|
# Signature is invalid
|
|
|
|
data["verified"] = False
|
|
|
|
|
|
|
|
# Send message to discord
|
|
|
|
send_discord_message(data)
|
|
|
|
save_vote(data)
|
2024-02-26 19:37:03 +11:00
|
|
|
|
|
|
|
vote_chart = ""
|
|
|
|
if info['public']:
|
|
|
|
vote_chart = render.votes()
|
|
|
|
else:
|
|
|
|
date = datetime.datetime.strptime(info['end'], "%Y-%m-%d")
|
|
|
|
if date < datetime.datetime.now():
|
|
|
|
vote_chart = render.votes()
|
|
|
|
|
|
|
|
|
|
|
|
return render_template('success.html', year=datetime.datetime.now().year, vote=data["message"],percent=percent,signature=signature,votes=vote_chart)
|
2024-02-19 15:18:49 +11:00
|
|
|
|
|
|
|
def save_vote(data):
|
|
|
|
# Load votes
|
|
|
|
with open('data/votes.json') as file:
|
|
|
|
votes = json.load(file)
|
|
|
|
|
|
|
|
address = data["walletAddress"]
|
|
|
|
# Remove old vote
|
|
|
|
for vote in votes:
|
|
|
|
if vote["walletAddress"] == address:
|
|
|
|
votes.remove(vote)
|
|
|
|
# Add new vote
|
|
|
|
votes.append(data)
|
|
|
|
|
|
|
|
# Save votes
|
|
|
|
with open('data/votes.json', 'w') as file:
|
|
|
|
json.dump(votes, file, indent=4)
|
|
|
|
|
|
|
|
def send_discord_message(data):
|
|
|
|
text = f"New vote for `{data['message']}`"
|
|
|
|
|
|
|
|
tokens = data['votes']
|
|
|
|
# Convert to have commas
|
|
|
|
tokens = "{:,}".format(int(tokens))
|
|
|
|
|
|
|
|
# Define the message content
|
|
|
|
message = {
|
|
|
|
"embeds": [
|
|
|
|
{
|
|
|
|
"title": "New Vote",
|
|
|
|
"description": text,
|
|
|
|
"color": 65280 if data["verified"] else 16711680,
|
|
|
|
"footer": {
|
|
|
|
"text": "Nathan.Woodburn/"
|
|
|
|
},
|
|
|
|
"fields": [
|
|
|
|
{
|
|
|
|
"name": "Wallet Address",
|
|
|
|
"value": '`'+data["walletAddress"]+'`'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"name": "Vote",
|
|
|
|
"value": '`'+data["message"]+'`'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"name": "Verified",
|
|
|
|
"value": "Yes" if data["verified"] else "No"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"name": "Signature",
|
|
|
|
"value": '`'+data["signature"]+'`'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"name": "Number of tokens",
|
|
|
|
"value": tokens
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
# Send the message as a POST request to the webhook URL
|
2024-02-25 21:39:04 +11:00
|
|
|
if DISCORD_WEBHOOK is not None:
|
|
|
|
requests.post(DISCORD_WEBHOOK, data=json.dumps(message), headers={'Content-Type': 'application/json'})
|
2024-02-19 15:18:49 +11:00
|
|
|
|
2024-02-25 21:46:57 +11:00
|
|
|
@app.route('/votes')
|
|
|
|
def download():
|
2024-02-26 19:37:03 +11:00
|
|
|
if 'walletAddress' in request.args:
|
|
|
|
address = request.args['walletAddress']
|
|
|
|
with open('data/votes.json') as file:
|
|
|
|
votes = json.load(file)
|
|
|
|
for vote in votes:
|
|
|
|
if vote["walletAddress"] == address:
|
|
|
|
return jsonify([vote])
|
|
|
|
return jsonify([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
info = get_vote_info()
|
|
|
|
if not info['public']:
|
|
|
|
end = datetime.datetime.strptime(info['end'], "%Y-%m-%d")
|
|
|
|
if end > datetime.datetime.now():
|
|
|
|
return render_template('blocked.html', year=datetime.datetime.now().year), 404
|
|
|
|
|
|
|
|
|
2024-02-25 21:46:57 +11:00
|
|
|
resp = make_response(send_from_directory('data', 'votes.json'))
|
|
|
|
# Set as json
|
|
|
|
resp.headers['Content-Type'] = 'application/json'
|
|
|
|
return resp
|
2024-02-19 15:18:49 +11:00
|
|
|
|
2024-02-26 19:37:03 +11:00
|
|
|
|
|
|
|
#region admin
|
|
|
|
@app.route('/login', methods=['POST'])
|
|
|
|
def login():
|
|
|
|
# If account.json doesn't exist, create it
|
|
|
|
if not os.path.isfile('data/account.json'):
|
|
|
|
|
|
|
|
user = request.form['email']
|
|
|
|
# Hash password
|
|
|
|
password = request.form['password']
|
|
|
|
hashed = hashlib.sha256(password.encode()).hexdigest()
|
|
|
|
token = random.randint(100000, 999999)
|
|
|
|
with open('data/account.json', 'w') as file:
|
|
|
|
json.dump({'email': user, 'password': hashed, 'token': token}, file)
|
|
|
|
resp = make_response(redirect('/admin'))
|
|
|
|
resp.set_cookie('token', str(token))
|
|
|
|
return resp
|
|
|
|
|
|
|
|
|
|
|
|
# Read account.json
|
|
|
|
with open('data/account.json') as file:
|
|
|
|
account = json.load(file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
user = request.form['email']
|
|
|
|
# Hash password
|
|
|
|
password = request.form['password']
|
|
|
|
hashed = hashlib.sha256(password.encode()).hexdigest()
|
|
|
|
|
|
|
|
if user == account['email'] and hashed == account['password']:
|
|
|
|
token = random.randint(100000, 999999)
|
|
|
|
account['token'] = token
|
|
|
|
with open('data/account.json', 'w') as file:
|
|
|
|
json.dump(account, file)
|
|
|
|
resp = make_response(redirect('/admin'))
|
|
|
|
resp.set_cookie('token', str(token))
|
|
|
|
return resp
|
|
|
|
|
|
|
|
return redirect('/')
|
|
|
|
|
|
|
|
@app.route('/admin')
|
|
|
|
def admin():
|
|
|
|
if not 'token' in request.cookies:
|
|
|
|
return redirect('/login')
|
|
|
|
with open('data/account.json') as file:
|
|
|
|
account = json.load(file)
|
|
|
|
if request.cookies['token'] != str(account['token']):
|
|
|
|
return redirect('/login')
|
|
|
|
|
|
|
|
info = get_vote_info()
|
|
|
|
options = ','.join(info['options'])
|
|
|
|
|
2024-02-26 23:32:34 +11:00
|
|
|
return render_template('admin.html', year=datetime.datetime.now().year, name=info['vote'],
|
|
|
|
description=info['description'], end=info['end'], start=info['start'],
|
|
|
|
enabled=info['enabled'], public=info['public'], revote=info['revote'], options=options)
|
2024-02-26 19:37:03 +11:00
|
|
|
|
|
|
|
@app.route('/admin', methods=['POST'])
|
|
|
|
def admin_post():
|
|
|
|
if not 'token' in request.cookies:
|
|
|
|
return redirect('/login')
|
|
|
|
with open('data/account.json') as file:
|
|
|
|
account = json.load(file)
|
|
|
|
if request.cookies['token'] != str(account['token']):
|
|
|
|
return redirect('/login')
|
|
|
|
|
|
|
|
info = get_vote_info()
|
|
|
|
|
|
|
|
info['vote'] = request.form['name']
|
|
|
|
info['description'] = request.form['description']
|
|
|
|
info['end'] = request.form['end']
|
2024-02-26 23:32:34 +11:00
|
|
|
info['start'] = request.form['start']
|
2024-02-26 19:37:03 +11:00
|
|
|
info['enabled'] = 'enabled' in request.form
|
|
|
|
info['public'] = 'public' in request.form
|
|
|
|
info['revote'] = 'revote' in request.form
|
|
|
|
options = request.form['options']
|
|
|
|
options = options.split(',')
|
|
|
|
info['options'] = options
|
|
|
|
|
|
|
|
with open('data/info.json', 'w') as file:
|
|
|
|
json.dump(info, file)
|
|
|
|
|
|
|
|
return redirect('/admin')
|
|
|
|
|
|
|
|
@app.route('/admin/clear')
|
|
|
|
def clear():
|
|
|
|
if not 'token' in request.cookies:
|
|
|
|
return redirect('/login')
|
|
|
|
with open('data/account.json') as file:
|
|
|
|
account = json.load(file)
|
|
|
|
if request.cookies['token'] != str(account['token']):
|
|
|
|
return redirect('/login')
|
|
|
|
with open('data/votes.json', 'w') as file:
|
|
|
|
json.dump([], file)
|
|
|
|
return redirect('/admin')
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
def get_vote_info():
|
|
|
|
if not os.path.isfile('data/info.json'):
|
2024-02-26 23:32:34 +11:00
|
|
|
end = datetime.datetime.now() + datetime.timedelta(days=7)
|
|
|
|
end = end.strftime("%Y-%m-%d")
|
|
|
|
start = datetime.datetime.now() - datetime.timedelta(days=7)
|
|
|
|
start = start.strftime("%Y-%m-%d")
|
2024-02-26 19:37:03 +11:00
|
|
|
with open('data/info.json', 'w') as file:
|
2024-02-26 23:32:34 +11:00
|
|
|
json.dump({'vote': '','description':'', 'end': end,'start':start,'enabled': False, 'public': True, 'revote': True, 'options': []}, file)
|
2024-02-26 19:37:03 +11:00
|
|
|
with open('data/info.json') as file:
|
|
|
|
info = json.load(file)
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
2024-02-19 15:18:49 +11:00
|
|
|
# 404 catch all
|
|
|
|
@app.errorhandler(404)
|
|
|
|
def not_found(e):
|
2024-02-19 15:28:51 +11:00
|
|
|
return render_template('404.html', year=datetime.datetime.now().year), 404
|
2024-02-19 15:18:49 +11:00
|
|
|
|
|
|
|
|
2024-02-26 23:32:34 +11:00
|
|
|
# Time helper functions
|
|
|
|
def timeLeft():
|
|
|
|
info = get_vote_info()
|
|
|
|
end = utc_now.strptime(info["end"], "%Y-%m-%d")
|
|
|
|
left = end - utc_now
|
|
|
|
print(left)
|
|
|
|
return left
|
|
|
|
|
|
|
|
def endTime():
|
|
|
|
info = get_vote_info()
|
|
|
|
end = utc_now.strptime(info["end"], "%Y-%m-%d")
|
|
|
|
return end
|
|
|
|
|
|
|
|
def startTime():
|
|
|
|
info = get_vote_info()
|
|
|
|
start = utc_now.strptime(info["start"], "%Y-%m-%d")
|
|
|
|
return start
|
|
|
|
|
|
|
|
def hasStarted():
|
|
|
|
start = startTime()
|
|
|
|
if utc_now > start:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def hasEnded():
|
|
|
|
end = endTime()
|
|
|
|
if utc_now > end:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-02-19 15:18:49 +11:00
|
|
|
if __name__ == '__main__':
|
|
|
|
app.run(debug=True, port=5000, host='0.0.0.0')
|