mirror of
https://github.com/jwinterm/tubbymemes.git
synced 2026-10-17 05:21:28 +02:00
reskinning for tubbycats
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from logging.config import dictConfig
|
||||
|
||||
from tubbymemes.factory import create_app
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
dictConfig(config.LOGGING_CONFIG)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
@@ -0,0 +1,126 @@
|
||||
import requests
|
||||
from flask import Blueprint
|
||||
from secrets import token_urlsafe
|
||||
from datetime import datetime
|
||||
|
||||
from tubbymemes.factory import db
|
||||
from tubbymemes.helpers import get_eth_contract
|
||||
from tubbymemes.models import Meme, User, Remote
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
bp = Blueprint('cli', 'cli', cli_group=None)
|
||||
|
||||
|
||||
@bp.cli.command('init')
|
||||
def init():
|
||||
db.create_all()
|
||||
|
||||
@bp.cli.command('sync-avax')
|
||||
def sync_avax():
|
||||
"""
|
||||
Synchronize your local database with the tokens minted on Avalanche blockchain.
|
||||
"""
|
||||
contract = get_eth_contract()
|
||||
total_supply = contract.functions.totalSupply().call()
|
||||
# walk backwards through all tokens at top of supply
|
||||
# until you've accounted for the last known
|
||||
for i in range(total_supply, 0, -1):
|
||||
# first get metadata ipfs hash
|
||||
deets = contract.functions.tokenMeme(i).call()
|
||||
try:
|
||||
meme_exists = Meme.query.filter(Meme.meta_ipfs_hash == deets[8]).first() # noqa
|
||||
user_exists = User.query.filter(User.public_address == deets[6].lower()).first() # noqa
|
||||
if meme_exists:
|
||||
if not meme_exists.minted:
|
||||
meme_exists.minted = True
|
||||
db.session.commit()
|
||||
print(f'[+] Marked existing meme {meme_exists.id} as minted')
|
||||
else:
|
||||
print(deets)
|
||||
if not user_exists:
|
||||
user_exists = User(
|
||||
public_address=deets[4].lower()
|
||||
)
|
||||
db.session.add(user_exists)
|
||||
db.session.commit()
|
||||
user_exists.handle = f'anon{user_exists.id}-{token_urlsafe(6)}'
|
||||
db.session.commit()
|
||||
print(f'[+] Created user {user_exists.handle}')
|
||||
res = requests.get(f'{config.IPFS_SERVER}/ipfs/{deets[5]}', timeout=30).json()
|
||||
if not 'image' in res:
|
||||
print('No image IPFS hash, skipping')
|
||||
continue
|
||||
meme_ipfs_hash = res['image'].split('ipfs://')[1]
|
||||
filename = token_urlsafe(24)
|
||||
print(f'[+] Downloading image hash {meme_ipfs_hash} as {filename}')
|
||||
r = requests.get(f'{config.IPFS_SERVER}/ipfs/{meme_ipfs_hash}', stream=True)
|
||||
with open(f'{config.DATA_FOLDER}/uploads/{filename}', 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size = 16*1024):
|
||||
f.write(chunk)
|
||||
meme = Meme(
|
||||
title=res['name'],
|
||||
file_name=filename,
|
||||
description=res['description'],
|
||||
user_id=user_exists.id,
|
||||
meta_ipfs_hash=deets[5],
|
||||
meme_ipfs_hash=meme_ipfs_hash,
|
||||
minted=True,
|
||||
synced=True
|
||||
)
|
||||
db.session.add(meme)
|
||||
db.session.commit()
|
||||
print(f'[+] Added new meme {meme.id}')
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
@bp.cli.command('sync-remotes')
|
||||
def sync_remotes():
|
||||
"""
|
||||
Sync remote servers with local database.
|
||||
"""
|
||||
from pprint import pprint
|
||||
remotes = Remote.query.filter(Remote.paused == False)
|
||||
for remote in remotes:
|
||||
print(f'[+] Fetching approved memes from remote {remote.id} ({remote.endpoint})')
|
||||
deets = requests.get(remote.endpoint + '/api/v1/memes').json()
|
||||
for meme_id in deets:
|
||||
meme = deets[meme_id]
|
||||
user = meme['user']
|
||||
meme_exists = Meme.query.filter(Meme.meta_ipfs_hash == meme['meta_ipfs_hash']).first() # noqa
|
||||
user_exists = User.query.filter(User.public_address == user['public_address'].lower()).first() # noqa
|
||||
if not meme_exists:
|
||||
if not user_exists:
|
||||
user_exists = User(
|
||||
public_address=user['public_address'].lower(),
|
||||
bio=user['bio'],
|
||||
ipfs_hash=user['ipfs_hash'],
|
||||
profile_image=user['profile_image'],
|
||||
website_url=user['website_url'],
|
||||
wownero_address=user['wownero_address'],
|
||||
handle=user['handle']
|
||||
)
|
||||
db.session.add(user_exists)
|
||||
db.session.commit()
|
||||
print(f'[+] Created user {user_exists.handle}')
|
||||
print(f'[+] Downloading image hash {meme["meme_ipfs_hash"]} as {meme["file_name"]}')
|
||||
r = requests.get(f'{config.IPFS_SERVER}/ipfs/{meme["meme_ipfs_hash"]}', stream=True)
|
||||
with open(f'{config.DATA_FOLDER}/uploads/{meme["file_name"]}', 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size = 16*1024):
|
||||
f.write(chunk)
|
||||
meme = Meme(
|
||||
title=meme['title'],
|
||||
file_name=meme['file_name'],
|
||||
description=meme['description'],
|
||||
user_id=user_exists.id,
|
||||
meta_ipfs_hash=meme['meta_ipfs_hash'],
|
||||
meme_ipfs_hash=meme['meme_ipfs_hash'],
|
||||
minted=meme['minted'],
|
||||
synced=True
|
||||
)
|
||||
db.session.add(meme)
|
||||
db.session.commit()
|
||||
print(f'[+] Added new meme {meme.id}')
|
||||
remote.last_sync_date = datetime.utcnow()
|
||||
db.session.commit()
|
||||
@@ -0,0 +1,54 @@
|
||||
import click
|
||||
from flask import Blueprint
|
||||
|
||||
from tubbymemes.models import Moderator, User
|
||||
from tubbymemes.factory import db
|
||||
|
||||
|
||||
bp = Blueprint('mod', 'mod')
|
||||
|
||||
|
||||
@bp.cli.command('list')
|
||||
def list():
|
||||
"""
|
||||
List current server moderators.
|
||||
"""
|
||||
for mod in Moderator.query.all():
|
||||
click.echo(mod.user.handle)
|
||||
|
||||
|
||||
@bp.cli.command('add')
|
||||
@click.argument('moderator_handle')
|
||||
def add(moderator_handle):
|
||||
"""
|
||||
Add server moderators by user handle.
|
||||
"""
|
||||
user = User.query.filter(User.handle == moderator_handle).first()
|
||||
if user:
|
||||
mod = Moderator.query.filter(Moderator.user_id == user.id).first()
|
||||
if mod is None:
|
||||
m = Moderator(user_id=user.id)
|
||||
db.session.add(m)
|
||||
db.session.commit()
|
||||
click.echo(f'[+] Added moderator status to `{moderator_handle}`')
|
||||
else:
|
||||
click.echo('[.] That is not a valid user.')
|
||||
|
||||
|
||||
@bp.cli.command('remove')
|
||||
@click.argument('moderator_handle')
|
||||
def remove(moderator_handle):
|
||||
"""
|
||||
Remove server moderator by user handle.
|
||||
"""
|
||||
user = User.query.filter(User.handle == moderator_handle).first()
|
||||
if user:
|
||||
mod = Moderator.query.filter(Moderator.user_id == user.id).first()
|
||||
if mod:
|
||||
db.session.delete(mod)
|
||||
db.session.commit()
|
||||
click.echo(f'[-] Removed moderator status from `{moderator_handle}`') # noqa
|
||||
else:
|
||||
click.echo('[.] That user is not a moderator.')
|
||||
else:
|
||||
click.echo('[.] That is not a valid user.')
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,57 @@
|
||||
from logging.config import dictConfig
|
||||
|
||||
from flask import Flask
|
||||
from flask_login import LoginManager
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_mobility import Mobility
|
||||
from web3 import Web3
|
||||
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
w3 = Web3(Web3.HTTPProvider(config.AVAX_RPC))
|
||||
|
||||
|
||||
def setup_db(app: Flask, db: SQLAlchemy = db):
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{config.DATA_FOLDER}/sqlite.db' # noqa
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
db.init_app(app)
|
||||
return db
|
||||
|
||||
|
||||
def create_app_huey():
|
||||
app = Flask(__name__)
|
||||
db = SQLAlchemy()
|
||||
dictConfig(config.LOGGING_CONFIG)
|
||||
setup_db(app, db)
|
||||
return app
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_envvar('FLASK_SECRETS')
|
||||
setup_db(app)
|
||||
Mobility(app)
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = 'meme.index'
|
||||
login_manager.logout_view = 'meta.disconnect'
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
from tubbymemes.models import User
|
||||
user = User.query.get(user_id)
|
||||
return user
|
||||
|
||||
with app.app_context():
|
||||
from tubbymemes import filters
|
||||
from tubbymemes.routes import api, meme, meta, user
|
||||
from tubbymemes.cli import mod, cli
|
||||
app.register_blueprint(filters.bp)
|
||||
app.register_blueprint(api.bp)
|
||||
app.register_blueprint(meme.bp)
|
||||
app.register_blueprint(meta.bp)
|
||||
app.register_blueprint(user.bp)
|
||||
app.register_blueprint(mod.bp)
|
||||
app.register_blueprint(cli.bp)
|
||||
return app
|
||||
@@ -0,0 +1,31 @@
|
||||
from flask import Blueprint
|
||||
from arrow import get as arrow_get
|
||||
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
bp = Blueprint('filters', 'filters')
|
||||
|
||||
|
||||
@bp.app_template_filter('shorten_address')
|
||||
def shorten_address(a):
|
||||
_p = a[0:6]
|
||||
_s = a[-4:]
|
||||
return f'{_p}...{_s}'
|
||||
|
||||
|
||||
@bp.app_template_filter('humanize')
|
||||
def humanize(d):
|
||||
if not d:
|
||||
return 'never'
|
||||
return arrow_get(d).humanize()
|
||||
|
||||
@bp.app_template_filter()
|
||||
def show_snowtrace(s) -> str:
|
||||
"""
|
||||
Return proper links to Etherscan based upon the currently configured network.
|
||||
"""
|
||||
if config.CONTRACT_TESTNET:
|
||||
return f'https://testnet.snowtrace.io/search?f=0&q={s}'
|
||||
else:
|
||||
return f'https://snowtrace.io/search?f=0&q={s}'
|
||||
@@ -0,0 +1,57 @@
|
||||
import ipfsApi
|
||||
from eth_account.messages import encode_defunct
|
||||
|
||||
from tubbymemes.models import Meme
|
||||
from tubbymemes.factory import w3
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
def get_eth_contract():
|
||||
"""
|
||||
Return a web3 contract object with the currently
|
||||
deployed tubbymemes smart contract.
|
||||
"""
|
||||
contract_abi = config.CONTRACT_ABI
|
||||
contract_address = w3.toChecksumAddress(config.CONTRACT_ADDRESS)
|
||||
return w3.eth.contract(
|
||||
address=contract_address,
|
||||
abi=contract_abi
|
||||
)
|
||||
|
||||
|
||||
def verify_signature(message, signature, public_address):
|
||||
msg = encode_defunct(text=message)
|
||||
recovered = w3.eth.account.recover_message(msg, signature=signature)
|
||||
if recovered.lower() == public_address.lower():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def upload_to_ipfs(meme_id: str):
|
||||
meme = Meme.query.get(meme_id)
|
||||
if not meme:
|
||||
return False
|
||||
try:
|
||||
client = ipfsApi.Client('127.0.0.1', 5001)
|
||||
artwork_hashes = client.add(meme.get_fs_path())
|
||||
artwork_hash = artwork_hashes[0]['Hash']
|
||||
print(f'[+] Uploaded artwork to IPFS: {artwork_hash}')
|
||||
meta = {
|
||||
'name': meme.title,
|
||||
'description': meme.description,
|
||||
'image': f'ipfs://{artwork_hash}',
|
||||
'create_date': meme.create_date.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'file_name': meme.file_name,
|
||||
'ipfs_hash': artwork_hash,
|
||||
'properties': {
|
||||
'creator_handle': meme.user.handle,
|
||||
'creator_address': meme.user.public_address
|
||||
}
|
||||
}
|
||||
meta_hash = client.add_json(meta)
|
||||
print(f'[+] Uploaded metadata to IPFS: {meta_hash}')
|
||||
return (meta_hash, artwork_hash)
|
||||
except Exception as e:
|
||||
print(f'[!] Error: {e}')
|
||||
return False
|
||||
@@ -0,0 +1,136 @@
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
from flask import url_for
|
||||
from flask_login import login_user
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from tubbymemes.factory import db
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
def rand_id():
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
class Moderator(db.Model):
|
||||
__tablename__ = 'moderators'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
user = db.relationship('User', back_populates='moderator')
|
||||
|
||||
def __rep__(self):
|
||||
return self.user.handle
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
register_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_login_date = db.Column(db.DateTime, nullable=True)
|
||||
verified = db.Column(db.Boolean, default=False)
|
||||
public_address = db.Column(db.String(180))
|
||||
nonce = db.Column(db.String(180), default=rand_id())
|
||||
nonce_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
handle = db.Column(db.String(40), unique=True)
|
||||
bio = db.Column(db.String(600), nullable=True)
|
||||
profile_image = db.Column(db.String(300), nullable=True)
|
||||
ipfs_hash = db.Column(db.String(100), nullable=True)
|
||||
wownero_address = db.Column(db.String(120), nullable=True)
|
||||
website_url = db.Column(db.String(120), nullable=True)
|
||||
moderator = db.relationship('Moderator', back_populates='user')
|
||||
memes = db.relationship('Meme', back_populates='user')
|
||||
|
||||
def as_dict(self):
|
||||
return {c.key: getattr(self, c.key)
|
||||
for c in inspect(self).mapper.column_attrs if c.key != 'nonce'}
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.handle)
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
return self.admin
|
||||
|
||||
def is_moderator(self):
|
||||
return len(self.moderator) > 0
|
||||
|
||||
def get_id(self):
|
||||
return self.id
|
||||
|
||||
def get_profile_image(self, full=True):
|
||||
if self.profile_image:
|
||||
if full:
|
||||
return url_for('meta.uploaded_file', filename=self.profile_image) # noqa
|
||||
else:
|
||||
return self.profile_image
|
||||
else:
|
||||
return '/static/img/logo.png'
|
||||
|
||||
def generate_nonce(self):
|
||||
return rand_id()
|
||||
|
||||
def change_nonce(self):
|
||||
self.nonce = rand_id()
|
||||
self.nonce_date = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
def login(self):
|
||||
self.change_nonce()
|
||||
self.last_login_date = datetime.utcnow()
|
||||
login_user(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Meme(db.Model):
|
||||
__tablename__ = 'memes'
|
||||
|
||||
id = db.Column(db.String(80), default=rand_id, primary_key=True)
|
||||
create_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
file_name = db.Column(db.String(200), unique=True)
|
||||
meta_ipfs_hash = db.Column(db.String(100), unique=True, nullable=True)
|
||||
meme_ipfs_hash = db.Column(db.String(100), unique=True, nullable=True)
|
||||
title = db.Column(db.String(50))
|
||||
description = db.Column(db.String(400), nullable=True)
|
||||
minted = db.Column(db.Boolean, default=False)
|
||||
approved = db.Column(db.Boolean, default=False)
|
||||
synced = db.Column(db.Boolean, default=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
user = db.relationship('User', back_populates='memes')
|
||||
|
||||
def as_dict(self):
|
||||
return {c.key: getattr(self, c.key)
|
||||
for c in inspect(self).mapper.column_attrs}
|
||||
|
||||
def __repr__(self):
|
||||
return str(f'meme-{self.id}')
|
||||
|
||||
def get_fs_path(self):
|
||||
return f'{config.DATA_FOLDER}/uploads/{self.file_name}'
|
||||
|
||||
|
||||
class Remote(db.Model):
|
||||
__tablename__ = 'remotes'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
create_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_sync_date = db.Column(db.DateTime, nullable=True)
|
||||
paused = db.Column(db.Boolean, default=False)
|
||||
endpoint = db.Column(db.String(120))
|
||||
|
||||
def __repr__(self):
|
||||
return str(f'remote-{self.id}')
|
||||
@@ -0,0 +1,152 @@
|
||||
from secrets import token_urlsafe
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_login import current_user
|
||||
|
||||
from tubbymemes.factory import db
|
||||
from tubbymemes.helpers import verify_signature
|
||||
from tubbymemes.models import User, Meme
|
||||
|
||||
|
||||
bp = Blueprint('api', 'api', url_prefix='/api/v1')
|
||||
|
||||
|
||||
@bp.route('/memes')
|
||||
def memes():
|
||||
"""
|
||||
List all the approved memes on the server to allow remote syncing.
|
||||
"""
|
||||
all_memes = dict()
|
||||
memes = Meme.query.filter(Meme.approved == True).all()
|
||||
for meme in memes:
|
||||
all_memes[meme.id] = meme.as_dict()
|
||||
all_memes[meme.id]['user'] = meme.user.as_dict()
|
||||
return jsonify(all_memes)
|
||||
|
||||
|
||||
@bp.route('/user_exists')
|
||||
def user_exists():
|
||||
"""
|
||||
Check to see if a given user exists (handle or wallet address).
|
||||
This logic will help the login/connect MetaMask flow.
|
||||
"""
|
||||
if 'public_address' in request.args:
|
||||
query_str = 'public_address'
|
||||
query_field = User.public_address
|
||||
elif 'handle' in request.args:
|
||||
query_str = 'handle'
|
||||
query_field = User.handle
|
||||
else:
|
||||
return jsonify({'success': False})
|
||||
|
||||
u = User.query.filter(
|
||||
query_field == request.args[query_str].lower()
|
||||
).first()
|
||||
if u:
|
||||
nonce = u.nonce
|
||||
else:
|
||||
nonce = User().generate_nonce()
|
||||
return jsonify({
|
||||
'user_exists': u is not None,
|
||||
'nonce': nonce,
|
||||
'query': query_str,
|
||||
'success': True
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/update/user', methods=['POST'])
|
||||
def update_user():
|
||||
if not current_user.is_authenticated:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Must be authenticated in order to update.'
|
||||
})
|
||||
|
||||
data = request.get_json()
|
||||
_u = User.query.get(int(data['user_id']))
|
||||
|
||||
if _u:
|
||||
if current_user.id == _u.id:
|
||||
user_exists = User.query.filter(
|
||||
User.handle == data['handle']
|
||||
).first()
|
||||
if user_exists and not current_user.id == user_exists.id:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'That user handle already exists on this server'
|
||||
})
|
||||
_u.wownero_address = data['wownero_address']
|
||||
_u.ipfs_hash = data['ipfs_hash']
|
||||
_u.handle = data['handle']
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Updated user record.'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Cannot edit another record.'
|
||||
})
|
||||
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'User does not exist.'
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/authenticate/metamask', methods=['POST'])
|
||||
def authenticate_metamask():
|
||||
"""
|
||||
This is the login/authenticate route for this dApp.
|
||||
Users POST a `signedData` blob, a message signed by the user with MetaMask
|
||||
(`personal_sign` method).
|
||||
|
||||
This route will verify the signed data against the user's public ETH
|
||||
address. If no user exists, they get an entry in the database with a
|
||||
default handle assigned. If user does exist, they get logged in.
|
||||
"""
|
||||
data = request.get_json()
|
||||
if current_user.is_authenticated:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Already registered and authenticated.'
|
||||
})
|
||||
|
||||
_u = User.query.filter_by(
|
||||
public_address=data['public_address'].lower()
|
||||
).first()
|
||||
|
||||
if _u:
|
||||
if data['message'].endswith(_u.nonce):
|
||||
if verify_signature(data['message'], data['signed_data'], data['public_address']): # noqa
|
||||
_u.login()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Logged in'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Invalid signature'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Invalid nonce in signed message'
|
||||
})
|
||||
else:
|
||||
rand_str = token_urlsafe(6)
|
||||
user = User(
|
||||
public_address=data['public_address'].lower()
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
user.handle = f'anon{user.id}-{rand_str}'
|
||||
db.session.commit()
|
||||
user.login()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Registered'
|
||||
})
|
||||
@@ -0,0 +1,158 @@
|
||||
from os import path, remove
|
||||
from secrets import token_urlsafe
|
||||
|
||||
import ipfsApi
|
||||
from flask import Blueprint, render_template, request
|
||||
from flask import redirect, flash, url_for
|
||||
from flask_login import current_user
|
||||
from web3 import Web3
|
||||
|
||||
from tubbymemes.models import Meme
|
||||
from tubbymemes.helpers import upload_to_ipfs
|
||||
from tubbymemes.factory import db
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
bp = Blueprint('meme', 'meme')
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
memes = Meme.query.filter(
|
||||
Meme.approved == True
|
||||
).order_by(Meme.create_date.desc())
|
||||
return render_template('index.html', memes=memes)
|
||||
|
||||
|
||||
@bp.route('/mod')
|
||||
def mod():
|
||||
if not current_user.is_authenticated:
|
||||
flash('You must be logged in and have MetaMask wallet connected.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
if not current_user.is_moderator():
|
||||
flash('You are not a moderator.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
memes = Meme.query.filter(
|
||||
Meme.approved != True
|
||||
).order_by(Meme.create_date.asc())
|
||||
return render_template('index.html', memes=memes)
|
||||
|
||||
|
||||
@bp.route('/publish', methods=['GET', 'POST'])
|
||||
def publish():
|
||||
if not current_user.is_authenticated:
|
||||
flash('You need to connect your wallet first.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
if not current_user.wownero_address:
|
||||
flash('You need to specify your Wownero wallet address first.', 'warning')
|
||||
return redirect(url_for('user.show', handle=current_user.handle))
|
||||
meme = None
|
||||
try:
|
||||
client = ipfsApi.Client('127.0.0.1', 5001)
|
||||
client.add_json({})
|
||||
except Exception as e:
|
||||
msg = f'[!] IPFS Error: {e}'
|
||||
print(msg)
|
||||
flash(msg, 'error')
|
||||
if "file" in request.files:
|
||||
return '<script>window.history.back()</script>'
|
||||
return redirect(url_for('meme.index') + '?ipfs_error=1')
|
||||
if "file" in request.files:
|
||||
title = request.form.get('title')
|
||||
description = request.form.get('description')
|
||||
file = request.files["file"]
|
||||
filename = "{}{}".format(
|
||||
token_urlsafe(24),
|
||||
path.splitext(file.filename)[1]
|
||||
)
|
||||
full_path = f'{config.DATA_FOLDER}/uploads/{filename}'
|
||||
file.save(full_path)
|
||||
try:
|
||||
meme = Meme(
|
||||
file_name=filename,
|
||||
title=title,
|
||||
description=description,
|
||||
user_id=current_user.id
|
||||
)
|
||||
db.session.add(meme)
|
||||
db.session.commit()
|
||||
if current_user.verified or current_user.is_moderator():
|
||||
res = upload_to_ipfs(meme.id)
|
||||
meme.meta_ipfs_hash = res[0]
|
||||
meme.meme_ipfs_hash = res[1]
|
||||
meme.approved = True
|
||||
db.session.commit()
|
||||
flash('Published new meme to local database and IPFS.', 'success') # noqa
|
||||
else:
|
||||
flash('Published new meme to database for review by moderators.', 'success') # noqa
|
||||
return redirect(url_for('meme.index'))
|
||||
except ConnectionError:
|
||||
flash('[!] Unable to connect to local ipfs', 'error')
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return render_template(
|
||||
'publish.html',
|
||||
meme=meme
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/meme/<meme_id>')
|
||||
def show(meme_id):
|
||||
meme = Meme.query.filter(Meme.id == meme_id).first()
|
||||
if not meme:
|
||||
return redirect('/')
|
||||
if not meme.approved and not current_user.is_authenticated:
|
||||
flash('You need to be logged in to view that meme.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
elif not meme.approved and not current_user.is_moderator():
|
||||
flash('You need to be a moderator to view that meme.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
return render_template('meme.html', meme=meme)
|
||||
|
||||
|
||||
@bp.route('/meme/<meme_id>/<action>')
|
||||
def approve(meme_id, action):
|
||||
if not current_user.is_authenticated:
|
||||
flash('You need to be logged in to reach this page.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
if not current_user.is_moderator():
|
||||
flash('You need to be a moderator to reach this page.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
meme = Meme.query.get(meme_id)
|
||||
if not meme:
|
||||
flash('That meme does not exist.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
if meme.approved is True:
|
||||
flash('That meme already has been approved.', 'warning')
|
||||
return redirect(url_for('meme.show', meme_id=meme.id))
|
||||
if action == 'approve':
|
||||
res = upload_to_ipfs(meme.id)
|
||||
if not res:
|
||||
flash('Unable to post to IPFS, daemon may be offline.', 'error')
|
||||
return redirect(url_for('meme.show', meme_id=meme.id))
|
||||
existing_meta_ipfs = Meme.query.filter(
|
||||
Meme.meta_ipfs_hash == res[0]
|
||||
).first()
|
||||
existing_meme_ipfs = Meme.query.filter(
|
||||
Meme.meme_ipfs_hash == res[1]
|
||||
).first()
|
||||
if meme.synced is False:
|
||||
if existing_meta_ipfs or existing_meme_ipfs:
|
||||
flash('Cannot use an existing IPFS hash for either metadata or memes on new posts.', 'warning') # noqa
|
||||
return redirect(url_for('meme.show', meme_id=meme.id))
|
||||
meme.meta_ipfs_hash = res[0]
|
||||
meme.meme_ipfs_hash = res[1]
|
||||
meme.approved = True
|
||||
db.session.commit()
|
||||
flash('Approved meme and published new meme to local IPFS server.', 'success')
|
||||
elif action == 'deny':
|
||||
# delete image
|
||||
# delete from database
|
||||
if path.exists(meme.get_fs_path()):
|
||||
remove(meme.get_fs_path())
|
||||
db.session.delete(meme)
|
||||
db.session.commit()
|
||||
flash('Deleted image and removed meme from database.', 'success')
|
||||
else:
|
||||
flash('Unknown action.', 'warning')
|
||||
return redirect(url_for('meme.mod'))
|
||||
@@ -0,0 +1,65 @@
|
||||
from flask import Blueprint, render_template, send_from_directory
|
||||
from flask import redirect, url_for, flash, request
|
||||
from flask_login import logout_user, current_user
|
||||
|
||||
from tubbymemes.models import User, Remote
|
||||
from tubbymemes.factory import db
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
bp = Blueprint('meta', 'meta')
|
||||
|
||||
|
||||
@bp.route('/uploads/<path:filename>')
|
||||
def uploaded_file(filename):
|
||||
"""
|
||||
Retrieve an uploaded file from uploads directory.
|
||||
"""
|
||||
return send_from_directory(f'{config.DATA_FOLDER}/uploads', filename)
|
||||
|
||||
|
||||
@bp.route('/about')
|
||||
def about():
|
||||
return render_template('about.html')
|
||||
|
||||
|
||||
@bp.route('/disconnect')
|
||||
def disconnect():
|
||||
logout_user()
|
||||
return redirect(url_for('meme.index'))
|
||||
|
||||
|
||||
@bp.route('/remotes', methods=['GET', 'POST'])
|
||||
def remotes():
|
||||
if not current_user.is_authenticated or not current_user.is_moderator():
|
||||
flash('You need to be moderator to access that page.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
remotes = Remote.query.filter().order_by(Remote.create_date.asc())
|
||||
ep = request.form.get('endpoint')
|
||||
if ep:
|
||||
if ep.startswith('http'):
|
||||
if not Remote.query.filter(Remote.endpoint == ep).first():
|
||||
r = Remote(endpoint=ep)
|
||||
db.session.add(r)
|
||||
db.session.commit()
|
||||
flash('Added new remote server.', 'success')
|
||||
else:
|
||||
flash('That remote server already exists.', 'warning')
|
||||
else:
|
||||
flash('Incorrect endpoint provided, must start with http or https.', 'warning') # noqa
|
||||
return render_template('remotes.html', remotes=remotes)
|
||||
|
||||
|
||||
@bp.route('/remotes/delete/<int:remote_id>')
|
||||
def delete_remote(remote_id):
|
||||
existing = Remote.query.get(remote_id)
|
||||
if not current_user.is_authenticated or not current_user.is_moderator():
|
||||
flash('You need to be moderator to access that page.', 'warning')
|
||||
return redirect(url_for('meme.index'))
|
||||
if not existing:
|
||||
flash('That remote does not exist', 'warning')
|
||||
return redirect(url_for('meme.mod'))
|
||||
db.session.delete(existing)
|
||||
db.session.commit()
|
||||
flash(f'Deleted remote server {existing}', 'success')
|
||||
return redirect(url_for('meta.remotes'))
|
||||
@@ -0,0 +1,15 @@
|
||||
from flask import Blueprint, render_template
|
||||
from flask import redirect, url_for
|
||||
|
||||
from tubbymemes.models import User
|
||||
|
||||
|
||||
bp = Blueprint('user', 'user')
|
||||
|
||||
|
||||
@bp.route('/user/<handle>')
|
||||
def show(handle):
|
||||
user = User.query.filter(User.handle == handle).first()
|
||||
if not user:
|
||||
return redirect(url_for('meme.index'))
|
||||
return render_template('profile.html', user=user)
|
||||
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
||||
.screen {
|
||||
padding-bottom: 1em;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
width: 300px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.image {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image img, .image video {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.img-fluid {
|
||||
max-height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.mintedAVAX {
|
||||
background-color: rgba(242,37,52,.2);
|
||||
border: 2px solid rgba(243,37,52,1);
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
.noty_theme__relax.noty_bar {
|
||||
margin: 4px 0;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
position: relative; }
|
||||
.noty_theme__relax.noty_bar .noty_body {
|
||||
padding: 10px; }
|
||||
.noty_theme__relax.noty_bar .noty_buttons {
|
||||
border-top: 1px solid #e7e7e7;
|
||||
padding: 5px 10px; }
|
||||
|
||||
.noty_theme__relax.noty_type__alert,
|
||||
.noty_theme__relax.noty_type__notification {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dedede;
|
||||
color: #444; }
|
||||
|
||||
.noty_theme__relax.noty_type__warning {
|
||||
background-color: #FFEAA8;
|
||||
border: 1px solid #FFC237;
|
||||
color: #826200; }
|
||||
.noty_theme__relax.noty_type__warning .noty_buttons {
|
||||
border-color: #dfaa30; }
|
||||
|
||||
.noty_theme__relax.noty_type__error {
|
||||
background-color: #FF8181;
|
||||
border: 1px solid #e25353;
|
||||
color: #FFF; }
|
||||
.noty_theme__relax.noty_type__error .noty_buttons {
|
||||
border-color: darkred; }
|
||||
|
||||
.noty_theme__relax.noty_type__info,
|
||||
.noty_theme__relax.noty_type__information {
|
||||
background-color: #78C5E7;
|
||||
border: 1px solid #3badd6;
|
||||
color: #FFF; }
|
||||
.noty_theme__relax.noty_type__info .noty_buttons,
|
||||
.noty_theme__relax.noty_type__information .noty_buttons {
|
||||
border-color: #0B90C4; }
|
||||
|
||||
.noty_theme__relax.noty_type__success {
|
||||
background-color: #BCF5BC;
|
||||
border: 1px solid #7cdd77;
|
||||
color: darkgreen; }
|
||||
.noty_theme__relax.noty_type__success .noty_buttons {
|
||||
border-color: #50C24E; }
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
.noty_layout_mixin, #noty_layout__top, #noty_layout__topLeft, #noty_layout__topCenter, #noty_layout__topRight, #noty_layout__bottom, #noty_layout__bottomLeft, #noty_layout__bottomCenter, #noty_layout__bottomRight, #noty_layout__center, #noty_layout__centerLeft, #noty_layout__centerRight {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 9999999;
|
||||
-webkit-transform: translateZ(0) scale(1, 1);
|
||||
transform: translateZ(0) scale(1, 1);
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
filter: blur(0);
|
||||
-webkit-filter: blur(0);
|
||||
max-width: 90%; }
|
||||
|
||||
#noty_layout__top {
|
||||
top: 0;
|
||||
left: 5%;
|
||||
width: 90%; }
|
||||
|
||||
#noty_layout__topLeft {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 325px; }
|
||||
|
||||
#noty_layout__topCenter {
|
||||
top: 5%;
|
||||
left: 50%;
|
||||
width: 325px;
|
||||
-webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
|
||||
transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
|
||||
|
||||
#noty_layout__topRight {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 325px; }
|
||||
|
||||
#noty_layout__bottom {
|
||||
bottom: 0;
|
||||
left: 5%;
|
||||
width: 90%; }
|
||||
|
||||
#noty_layout__bottomLeft {
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
width: 325px; }
|
||||
|
||||
#noty_layout__bottomCenter {
|
||||
bottom: 5%;
|
||||
left: 50%;
|
||||
width: 325px;
|
||||
-webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
|
||||
transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
|
||||
|
||||
#noty_layout__bottomRight {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 325px; }
|
||||
|
||||
#noty_layout__center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 325px;
|
||||
-webkit-transform: translate(-webkit-calc(-50% - .5px), -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
|
||||
transform: translate(calc(-50% - .5px), calc(-50% - .5px)) translateZ(0) scale(1, 1); }
|
||||
|
||||
#noty_layout__centerLeft {
|
||||
top: 50%;
|
||||
left: 20px;
|
||||
width: 325px;
|
||||
-webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
|
||||
transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
|
||||
|
||||
#noty_layout__centerRight {
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
width: 325px;
|
||||
-webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
|
||||
transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
|
||||
|
||||
.noty_progressbar {
|
||||
display: none; }
|
||||
|
||||
.noty_has_timeout.noty_has_progressbar .noty_progressbar {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
background-color: #646464;
|
||||
opacity: 0.2;
|
||||
filter: alpha(opacity=10); }
|
||||
|
||||
.noty_bar {
|
||||
-webkit-backface-visibility: hidden;
|
||||
-webkit-transform: translate(0, 0) translateZ(0) scale(1, 1);
|
||||
-ms-transform: translate(0, 0) scale(1, 1);
|
||||
transform: translate(0, 0) scale(1, 1);
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
overflow: hidden; }
|
||||
|
||||
.noty_effects_open {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate(50%);
|
||||
-ms-transform: translate(50%);
|
||||
transform: translate(50%);
|
||||
-webkit-animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards; }
|
||||
|
||||
.noty_effects_close {
|
||||
-webkit-animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards; }
|
||||
|
||||
.noty_fix_effects_height {
|
||||
-webkit-animation: noty_anim_height 75ms ease-out;
|
||||
animation: noty_anim_height 75ms ease-out; }
|
||||
|
||||
.noty_close_with_click {
|
||||
cursor: pointer; }
|
||||
|
||||
.noty_close_button {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-weight: bold;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
-webkit-transition: all .2s ease-out;
|
||||
transition: all .2s ease-out; }
|
||||
|
||||
.noty_close_button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1); }
|
||||
|
||||
.noty_modal {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
z-index: 10000;
|
||||
opacity: .3;
|
||||
left: 0;
|
||||
top: 0; }
|
||||
|
||||
.noty_modal.noty_modal_open {
|
||||
opacity: 0;
|
||||
-webkit-animation: noty_modal_in .3s ease-out;
|
||||
animation: noty_modal_in .3s ease-out; }
|
||||
|
||||
.noty_modal.noty_modal_close {
|
||||
-webkit-animation: noty_modal_out .3s ease-out;
|
||||
animation: noty_modal_out .3s ease-out;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards; }
|
||||
|
||||
@-webkit-keyframes noty_modal_in {
|
||||
100% {
|
||||
opacity: .3; } }
|
||||
|
||||
@keyframes noty_modal_in {
|
||||
100% {
|
||||
opacity: .3; } }
|
||||
|
||||
@-webkit-keyframes noty_modal_out {
|
||||
100% {
|
||||
opacity: 0; } }
|
||||
|
||||
@keyframes noty_modal_out {
|
||||
100% {
|
||||
opacity: 0; } }
|
||||
|
||||
@keyframes noty_modal_out {
|
||||
100% {
|
||||
opacity: 0; } }
|
||||
|
||||
@-webkit-keyframes noty_anim_in {
|
||||
100% {
|
||||
-webkit-transform: translate(0);
|
||||
transform: translate(0);
|
||||
opacity: 1; } }
|
||||
|
||||
@keyframes noty_anim_in {
|
||||
100% {
|
||||
-webkit-transform: translate(0);
|
||||
transform: translate(0);
|
||||
opacity: 1; } }
|
||||
|
||||
@-webkit-keyframes noty_anim_out {
|
||||
100% {
|
||||
-webkit-transform: translate(50%);
|
||||
transform: translate(50%);
|
||||
opacity: 0; } }
|
||||
|
||||
@keyframes noty_anim_out {
|
||||
100% {
|
||||
-webkit-transform: translate(50%);
|
||||
transform: translate(50%);
|
||||
opacity: 0; } }
|
||||
|
||||
@-webkit-keyframes noty_anim_height {
|
||||
100% {
|
||||
height: 0; } }
|
||||
|
||||
@keyframes noty_anim_height {
|
||||
100% {
|
||||
height: 0; } }
|
||||
|
||||
/*# sourceMappingURL=noty.css.map*/
|
||||
|
||||
|
||||
/* Custom */
|
||||
.noty_body {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
up-wrapper{display:inline-block}
|
||||
|
||||
up-bounds{position:absolute}.up-focusable-content:focus,.up-focusable-content:focus-visible{outline:0}
|
||||
|
||||
.up-request-loader{display:none}up-progress-bar{position:fixed;top:0;left:0;z-index:999999999;height:3px;background-color:#007bff}
|
||||
|
||||
up-focus-trap{position:fixed;top:0;left:0;width:0;height:0}up-modal,up-drawer,up-cover,up-modal-backdrop,up-drawer-backdrop,up-modal-viewport,up-drawer-viewport,up-cover-viewport{top:0;left:0;bottom:0;right:0}up-modal-box,up-drawer-box{box-shadow:0 0 10px 1px rgba(0,0,0,0.3)}up-popup{box-shadow:0 0 4px rgba(0,0,0,0.3)}up-modal:focus,up-drawer:focus,up-cover:focus,up-modal-box:focus,up-drawer-box:focus,up-cover-box:focus,up-popup:focus,up-modal:focus-visible,up-drawer:focus-visible,up-cover:focus-visible,up-modal-box:focus-visible,up-drawer-box:focus-visible,up-cover-box:focus-visible,up-popup:focus-visible{outline:none}up-modal,up-drawer,up-cover{z-index:2000;position:fixed}up-modal-backdrop,up-drawer-backdrop{position:absolute;background:rgba(0,0,0,0.4)}up-modal-viewport,up-drawer-viewport,up-cover-viewport{position:absolute;overflow-y:scroll;overflow-x:hidden;overscroll-behavior:contain;text-align:center}up-modal-box,up-drawer-box,up-cover-box,up-popup{display:inline-block;text-align:left;position:relative;box-sizing:border-box;max-width:100%;background-color:#fff;padding:20px;overflow-x:hidden}up-modal-content,up-drawer-content,up-cover-content,up-popup-content{display:block}up-popup{z-index:1000}up-modal-dismiss,up-drawer-dismiss,up-cover-dismiss,up-popup-dismiss{color:#888;position:absolute;top:10px;right:10px;font-size:1.7rem;line-height:0.5}up-modal-viewport{justify-content:center}up-modal[nesting="0"] up-modal-viewport{padding:25px 15px}up-modal[nesting="1"] up-modal-viewport{padding:50px 30px}up-modal[nesting="2"] up-modal-viewport{padding:75px 45px}up-modal[nesting="3"] up-modal-viewport{padding:100px 60px}up-modal[nesting="4"] up-modal-viewport{padding:125px 75px}up-modal[size=small] up-modal-box{width:350px}up-modal[size=medium] up-modal-box{width:650px}up-modal[size=large] up-modal-box{width:1000px}up-modal[size=grow] up-modal-box{width:auto}up-modal[size=full] up-modal-box{width:100%}up-drawer-viewport{text-align:left}up-drawer[position=right] up-drawer-viewport{text-align:right}up-drawer-box{min-height:100vh}up-drawer[size=small] up-drawer-box{width:150px}up-drawer[size=medium] up-drawer-box{width:340px}up-drawer[size=large] up-drawer-box{width:600px}up-drawer[size=grow] up-drawer-box{width:auto}up-drawer[size=full] up-drawer-box{width:100%}up-cover-box{width:100%;min-height:100vh;padding:0}up-popup{padding:15px}up-popup[size=small]{width:180px}up-popup[size=medium]{width:300px}up-popup[size=large]{width:550px}up-popup[size=grow] up-popup{width:auto}up-popup[size=full] up-popup{width:100%}
|
||||
|
||||
[up-href],[up-clickable]{cursor:pointer}
|
||||
|
||||
+9
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,10 @@
|
||||
async function notif(s, t) {
|
||||
new Noty({
|
||||
type: t,
|
||||
theme: 'relax',
|
||||
layout: 'topCenter',
|
||||
text: s,
|
||||
timeout: 4500
|
||||
}).show();
|
||||
return
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+3124
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+58
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
from huey import RedisHuey
|
||||
|
||||
from tubbymemes.factory import create_app_huey
|
||||
from tubbymemes import config
|
||||
|
||||
|
||||
huey = RedisHuey(
|
||||
host=config.CACHE_HOST,
|
||||
port=config.CACHE_PORT
|
||||
)
|
||||
|
||||
app = create_app_huey()
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{% include 'includes/head.html' %}
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% include 'includes/navbar.html' %}
|
||||
|
||||
<div class="columns">
|
||||
<div class="column screen" style="padding-top:1em;">
|
||||
<p>
|
||||
Welcome. </br>
|
||||
</br>
|
||||
This service is a continuation of <a href="https://suchwow.xyz">SuchWow</a> for the <a href="https://wownero.org">Wownero</a> cryptocurrency project.
|
||||
It has been revamped to use <a href="https://ipfs.io">Interplanetary Filesystem</a>, server federation / content sharing, and optionally <a href="https://www.avax.network/">Avalanche network</a> via a smart contract with some useful features (if you're into memes). It's a decentralized application (dApp); it's preferred you run it locally and bootstrap the services on your own machine. Here's the source code and instructions to run: <a href="https://github.com/lalanza808/tubbymemes/" target="_blank">tubbymemes</a>
|
||||
</p>
|
||||
</br>
|
||||
<p>
|
||||
Up until now there has been a small group of people who were responsible for reviewing and approving the 4600+ memes submitted to the prior platform.
|
||||
Wownero has become a meme powerhouse via SuchWow, but it's a highly centralized, aging, and fragile service.
|
||||
</p>
|
||||
</br>
|
||||
<p>
|
||||
This iteration of SuchWow will strive to provide decentralized and leaderless operations.
|
||||
You can run it anywhere and see all the content, you won't need my server (but you can use it).
|
||||
You should run your own.
|
||||
You can control which content you see, store, and share (via IPFS pins).
|
||||
I'll deploy an immutable smart contract with some tipping and storage features,
|
||||
everyone can make their own clients (you can use mine, it's Flask/Python),
|
||||
we make some documentation for you fucks to read (lol),
|
||||
then memers can run it and go to the races; the only limit is their own hard drives.
|
||||
</p>
|
||||
</br>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<a class="button is-info" onclick="addWOWX()">Add WOWX Tokens</a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a class="button is-info" onclick="addAvalancheNetwork()">Add Avalanche Network</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% include 'includes/footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
<up-modal size="medium"> <!-- container with attributes -->
|
||||
<up-modal-backdrop></up-modal-backdrop> <!-- semi-transparent background -->
|
||||
<up-modal-viewport> <!-- scroll bar -->
|
||||
<up-modal-box> <!-- white box with padding -->
|
||||
<up-modal-content>
|
||||
<p>Wownero Address: <span id="wowneroAddress">?</span></p>
|
||||
</up-modal-content> <!-- content parent (unstyled) -->
|
||||
<up-modal-dismiss>×</up-modal-dismiss> <!-- dismiss icon -->
|
||||
</up-modal-box>
|
||||
</up-modal-viewport>
|
||||
</up-modal>
|
||||
@@ -0,0 +1,6 @@
|
||||
<footer style="margin: 0 auto; width: 100%; text-align: center;">
|
||||
<p>Made by <a href="https://lzahq.tech" target="_blank">@lza_menace</a></p>
|
||||
</footer>
|
||||
|
||||
|
||||
{% include 'includes/scripts.html' %}
|
||||
@@ -0,0 +1,23 @@
|
||||
<head>
|
||||
<title>tubbymemes Interplanetary Memes</title>
|
||||
<meta property="og:description" content="Interplanetary meme platform powered by IPFS, Wownero, Avalanche, and degeneracy.">
|
||||
<meta property="og:image" content="https://tubbymemes.xyz/static/img/logo.png">
|
||||
<meta property="og:title" content="tubbymemes">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:site" content="@lza_menace">
|
||||
<meta name="twitter:title" content="tubbymemes">
|
||||
<meta name="twitter:description" content="Interplanetary meme platform powered by IPFS, Wownero, Avalanche, and degeneracy.">
|
||||
<meta name="twitter:creator" content="@lza_menace">
|
||||
<meta name="twitter:image" content="https://tubbymemes.xyz/static/img/logo.png">
|
||||
<meta name="twitter:domain" content="https://tubbymemes.xyz">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="/static/js/vendor/unpoly-2.5.0.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/vendor/unpoly-2.5.0.min.css">
|
||||
<link rel="stylesheet" href="/static/css/bulma.min.css">
|
||||
<!-- <link rel="stylesheet" href="/static/css/bulma.css.map"> -->
|
||||
<link rel="stylesheet" href="/static/css/vendor/noty-relax.css" />
|
||||
<link rel="stylesheet" href="/static/css/vendor/noty.css" />
|
||||
<link rel="stylesheet" href="/static/css/custom.css" />
|
||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||
</head>
|
||||
@@ -0,0 +1,53 @@
|
||||
{# expects meme var #}
|
||||
<div class="card {% if meme.minted %}mintedAVAX{% endif %}">
|
||||
<div class="card-image">
|
||||
<figure class="image">
|
||||
<a href="{{ url_for('meme.show', meme_id=meme.id) }}">
|
||||
{% if meme.file_name %}
|
||||
{% if meme.file_name.endswith('mp4') %}
|
||||
<video class="img-fluid" {% if not request.MOBILE %}autoplay{% else %}controls{% endif %} muted loop>
|
||||
<source src="{{ url_for('meta.uploaded_file', filename=meme.file_name) }}" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
{% else %}
|
||||
<img alt="{{ meme.title }}" src="{{ url_for('meta.uploaded_file', filename=meme.file_name) }}" width="200px" class="img-fluid" style="" />
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img alt="{{ meme.title }}" src="{{ config.IPFS_SERVER }}/ipfs/{{ meme.meme_ipfs_hash }}" width="200px" class="img-fluid" style="" />
|
||||
{% endif %}
|
||||
</a>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
{% if meme.user.profile_image %}
|
||||
<figure class="image is-48x48">
|
||||
<img src="{{ url_for('meta.uploaded_file', filename=meme.user.profile_image) }}" alt="{{ meme.user.handle }} pfp">
|
||||
</figure>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{ meme.title }}</p>
|
||||
<p class="subtitle is-6">
|
||||
<a href="{{ url_for('user.show', handle=meme.user.handle) }}">{{ meme.user.handle }}</a>
|
||||
<a href="{{ meme.user.public_address | show_snowtrace }}" target="_blank">({{ meme.user.public_address | shorten_address }})</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<time datetime="2016-1-1">{{ meme.create_date.strftime('%H:%M UTC - %d %b %Y') }}</time>
|
||||
{% if not meme.approved %}
|
||||
<div class="columns mt-2">
|
||||
<div class="column">
|
||||
<a class="button is-success" href="{{ url_for('meme.approve', meme_id=meme.id, action='approve') }}">Approve</a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a class="button is-danger" href="{{ url_for('meme.approve', meme_id=meme.id, action='deny') }}">Deny</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="screen">
|
||||
<h1 class="title">
|
||||
tubbymemes.
|
||||
</h1>
|
||||
<p class="subtitle">
|
||||
Memes. <strong>Interplanetary</strong>!
|
||||
</p>
|
||||
{% if request.path == '/' %}
|
||||
<a class="button is-secondary" href="{{ url_for('meta.about') }}" up-target=".container">About</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a class="button is-secondary" href="{{ url_for('user.show', handle=current_user.handle) }}">Profile</a>
|
||||
<a class="button is-danger" href="{{ url_for('meta.disconnect') }}">Disconnect</a>
|
||||
{% if current_user.is_moderator() %}
|
||||
<a class="button is-warning" href="{{ url_for('meme.mod') }}" up-preload up-follow=".container">Mod</a>
|
||||
{% endif %}
|
||||
<a class="button is-primary" href="{{ url_for('meme.publish') }}">New Meme</a>
|
||||
{% else %}
|
||||
<a class="button is-primary" id="metamaskConnect" onclick="onboardMetaMask();">Connect</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a class="button" href="{{ url_for('meme.index') }}" up-preload up-follow=".container">Go Home</a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.path == '/remotes' %}
|
||||
<a class="button is-warning" href="{{ url_for('meme.mod') }}" up-preload up-follow=".container">Mod</a>
|
||||
{% endif %}
|
||||
{% if request.path == '/mod' %}
|
||||
<a class="button is-warning" href="{{ url_for('meta.remotes') }}" up-preload up-follow=".container">Remotes</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script src="/static/js/vendor/metamask-onboarding-1.0.1.bundle.js"></script>
|
||||
<script src="/static/js/vendor/noty-3.2.0.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
{% include 'includes/web3.html' %}
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<script type="text/javascript">
|
||||
{% for category, message in messages %}
|
||||
{% if category == None %}{% set category = 'info' %}{% endif %}
|
||||
new Noty({
|
||||
type: '{{ category }}',
|
||||
theme: 'relax',
|
||||
layout: 'topCenter',
|
||||
text: '{{ message }}',
|
||||
timeout: 4500
|
||||
}).show();
|
||||
{% endfor %}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
@@ -0,0 +1,206 @@
|
||||
<script src="/static/js/vendor/web3-1.3.6.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// Arm connect button
|
||||
onboardMetaMask();
|
||||
|
||||
// Detect account changes in MetaMask
|
||||
if (MetaMaskOnboarding.isMetaMaskInstalled()) {
|
||||
window.ethereum.on('accountsChanged', (newAccounts) => {
|
||||
window.location.href = '/disconnect';
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
// Web3 and smart contract details
|
||||
let contractABI = {{ config.CONTRACT_ABI | tojson }};
|
||||
let contractAddress = "{{ config.CONTRACT_ADDRESS }}";
|
||||
let wowxABI = {{ config.WOWX_ABI | tojson }};
|
||||
let wowxAddress = "{{ config.WOWX_ADDRESS }}";
|
||||
const w3 = new Web3(Web3.givenProvider);
|
||||
const contract = new w3.eth.Contract(contractABI, contractAddress);
|
||||
const wowxContract = new w3.eth.Contract(wowxABI, wowxAddress);
|
||||
|
||||
async function getMetamaskAccount() {
|
||||
const accounts = await window.ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
});
|
||||
const account = accounts[0];
|
||||
return account
|
||||
}
|
||||
|
||||
async function getSignedData(publicAddress, jsonData) {
|
||||
const signedData = await window.ethereum.request({
|
||||
method: 'eth_signTypedData_v3',
|
||||
params: [publicAddress, JSON.stringify(jsonData)]
|
||||
});
|
||||
console.log(signedData);
|
||||
return signedData
|
||||
}
|
||||
|
||||
async function onboardMetaMask(){
|
||||
const onboarding = new MetaMaskOnboarding();
|
||||
const onboardButton = document.getElementById('metamaskConnect');
|
||||
let accounts;
|
||||
let nonce = 0;
|
||||
|
||||
if (!onboardButton) {
|
||||
return false;
|
||||
}
|
||||
|
||||
onboardButton.onclick = async () => {
|
||||
if (!MetaMaskOnboarding.isMetaMaskInstalled()) {
|
||||
onboardButton.onclick = () => {
|
||||
onboardButton.classList.add('is-loading');
|
||||
onboardButton.disabled = true;
|
||||
onboarding.startOnboarding();
|
||||
};
|
||||
} else if (accounts && accounts.length > 0) {
|
||||
onboardButton.classList.remove('is-loading');
|
||||
onboardButton.disabled = false;
|
||||
onboarding.stopOnboarding();
|
||||
} else {
|
||||
try {
|
||||
onboardButton.classList.add('is-loading');
|
||||
onboardButton.disabled = true;
|
||||
let userExists;
|
||||
await addAvalancheNetwork();
|
||||
const allAccounts = await window.ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
});
|
||||
await fetch('/api/v1/user_exists?public_address=' + allAccounts[0])
|
||||
.then((resp) => resp.json())
|
||||
.then(function(data) {
|
||||
if (!data['success']) {
|
||||
console.log('error checking user_exists!')
|
||||
return
|
||||
}
|
||||
console.log(data);
|
||||
nonce = data['nonce'];
|
||||
})
|
||||
|
||||
const msg = 'Authentication request from tubbymemes app! Verifying message with nonce ' + nonce
|
||||
const signedData = await window.ethereum.request({
|
||||
method: 'personal_sign',
|
||||
params: [msg, allAccounts[0]]
|
||||
});
|
||||
console.log(`Signing data with msg "${msg}", address "${allAccounts[0]}", signed data: ${signedData}`)
|
||||
|
||||
await fetch('/api/v1/authenticate/metamask', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'signed_data': signedData,
|
||||
'public_address': allAccounts[0],
|
||||
'nonce': nonce,
|
||||
'message': msg,
|
||||
})
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then(function(data) {
|
||||
console.log(data);
|
||||
if (data['success']) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
})
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
onboardButton.classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// connectButton();
|
||||
|
||||
};
|
||||
|
||||
|
||||
async function addWOWX() {
|
||||
let tokenAddress = '{{ config.WOWX_ADDRESS }}';
|
||||
try {
|
||||
await ethereum.request({
|
||||
method: 'wallet_watchAsset',
|
||||
params: {
|
||||
type: 'ERC20',
|
||||
options: {
|
||||
address: tokenAddress.toLowerCase(),
|
||||
symbol: 'WOWX',
|
||||
decimals: 18,
|
||||
image: 'https://github.com/wownerox/meta/raw/main/logo.png'
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function addAvalancheNetwork(){
|
||||
let chainId, rpcUrl, explorerUrl, name;
|
||||
if ('{{ config.CONTRACT_TESTNET }}' == 'True') {
|
||||
name = 'Avalanche Testnet';
|
||||
chainId = '0xA869';
|
||||
rpcUrl = 'https://api.avax-test.network/ext/bc/C/rpc';
|
||||
explorerUrl = 'https://testnet.snowtrace.io';
|
||||
} else {
|
||||
name = 'Avalanche Mainnet';
|
||||
chainId = '0xA86A';
|
||||
rpcUrl = 'https://api.avax.network/ext/bc/C/rpc';
|
||||
explorerUrl = 'https://snowtrace.io';
|
||||
}
|
||||
try {
|
||||
await ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: chainId }],
|
||||
});
|
||||
} catch (switchError) {
|
||||
// This error code indicates that the chain has not been added to MetaMask.
|
||||
if (switchError.code === 4902) {
|
||||
try {
|
||||
await ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: chainId,
|
||||
chainName: name,
|
||||
nativeCurrency: {
|
||||
name: 'Avalanche',
|
||||
symbol: 'AVAX',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: [rpcUrl],
|
||||
blockExplorerUrls: [explorerUrl],
|
||||
}],
|
||||
});
|
||||
} catch (addError) {
|
||||
// handle "add" error
|
||||
}
|
||||
}
|
||||
// handle other "switch" errors
|
||||
}
|
||||
}
|
||||
|
||||
async function getTokenID(ipfsHash) {
|
||||
await addAvalancheNetwork();
|
||||
if (ipfsHash != '') {
|
||||
const tokenId = await contract.methods.metadataTokenId(ipfsHash).call();
|
||||
return tokenId;
|
||||
}
|
||||
}
|
||||
|
||||
async function isMinted(ipfsHash) {
|
||||
await addAvalancheNetwork();
|
||||
if (ipfsHash != '') {
|
||||
const tokenId = await getTokenID(ipfsHash)
|
||||
if (tokenId == 0) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{% include 'includes/head.html' %}
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% include 'includes/navbar.html' %}
|
||||
|
||||
{% if request.args.get('ipfs_error') %}
|
||||
<div class="screen">
|
||||
<p>
|
||||
You can run your own local IPFS instance and maintain metadata and artwork,
|
||||
try installing the software and running the following:
|
||||
</p>
|
||||
<code>$ ipfs daemon</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if memes %}
|
||||
{% for _meme in memes | batch(4) %}
|
||||
<div class="columns">
|
||||
{% for meme in _meme %}
|
||||
{% include 'includes/meme_card.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{% include 'includes/footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,322 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{% include 'includes/head.html' %}
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
|
||||
{% include 'includes/navbar.html' %}
|
||||
|
||||
{% if meme %}
|
||||
{% if meme.meta_ipfs_hash %}
|
||||
{% set meta_url = config.IPFS_SERVER + "/ipfs/" + meme.meta_ipfs_hash %}
|
||||
{% set meme_url = config.IPFS_SERVER + "/ipfs/" + meme.meme_ipfs_hash %}
|
||||
{% else %}
|
||||
{% set meta_url = "" %}
|
||||
{% set meme_url = url_for('meta.uploaded_file', filename=meme.file_name, _external=True) %}
|
||||
{% endif %}
|
||||
|
||||
<div id="screen">
|
||||
<div class="screen">
|
||||
{% if meme.file_name %}
|
||||
{% if meme.file_name.endswith('mp4') %}
|
||||
<video style="max-height: 60vh!important;max-width:100%;" {% if not request.MOBILE %}autoplay{% else %}controls{% endif %} muted loop>
|
||||
<source src="{{ meme_url }}" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
{% else %}
|
||||
<img src="{{ meme_url }}" id="memeImage" />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br/>
|
||||
<p><strong>Meme ID:</strong> <code>{{ meme }}</code></p>
|
||||
<p><strong>Title:</strong> {{ meme.title }}</p>
|
||||
<p><strong>Description:</strong> {{ meme.description }}</p>
|
||||
<p><strong>Poster Address:</strong> <a href="{{ url_for('user.show', handle=meme.user.handle) }}">{{ meme.user.handle }}</a></p>
|
||||
{% if meme.user.wownero_address %}
|
||||
<p><strong>Wownero Address:</strong> <span style="word-wrap: break-word">{{ meme.user.wownero_address }}</p>
|
||||
{% endif %}
|
||||
{% if meme.meta_ipfs_hash %}
|
||||
<p><strong>Meta IPFS:</strong> <a href="{{ meta_url }}" target=_self>{{ meme.meta_ipfs_hash }}</a></p>
|
||||
{% endif %}
|
||||
{% if meme.meme_ipfs_hash %}
|
||||
<p><strong>Meme IPFS:</strong> <a href="{{ meme_url }}" target=_self>{{ meme.meme_ipfs_hash }}</a></p>
|
||||
{% endif %}
|
||||
{% if meme.minted %}
|
||||
<p>
|
||||
<strong>Tips Received:</strong>
|
||||
<span id="wowTips">?</span> WOW,
|
||||
<span id="wowxTips">?</span> WOWX,
|
||||
<span id="avaxTips">?</span> AVAX
|
||||
</p>
|
||||
<p><strong>Token ID:</strong> <span id="tokenId">? </span> <a href="{{ config.CONTRACT_ADDRESS | show_snowtrace }}">(contract)</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if not meme.approved %}
|
||||
<br/>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<a class="button is-success is-12 column" href="{{ url_for('meme.approve', meme_id=meme.id, action='approve') }}">Approve</a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a class="button is-danger is-12 column" href="{{ url_for('meme.approve', meme_id=meme.id, action='deny') }}">Deny</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<br/>
|
||||
<div id="tipButtons" class="columns" style="display:none;">
|
||||
{% if meme.user.wownero_address %}
|
||||
<div class="column">
|
||||
<a class="button is-info is-12" onclick="tipWOW()" id="tipWOWButton" style="width:100%;">Tip via p2p WOW</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="column">
|
||||
<a class="button is-info is-12" onclick="tipSC()" id="tipSCButton" style="width:100%;">Tip via Smart Contract</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mintButtons" class="columns" style="display:none;">
|
||||
{% if meme.user.wownero_address %}
|
||||
<div class="column is-one-third">
|
||||
<a class="button is-info" onclick="tipWOW()" id="tipWOWButton" style="width:100%;">Tip via p2p WOW</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="column">
|
||||
<a class="button is-danger" onclick="mint()" id="mintButton" style="width:100%;">Mint to AVAX</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="tipWOWModal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Tip via WOW p2p</p>
|
||||
<button class="delete" aria-label="close" id="closeWOWModal"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<p style="word-wrap: break-word;">
|
||||
<strong>Wownero Address (p2p):</strong>
|
||||
<span id="wowneroAddress">{{ meme.user.wownero_address | default('?') }}</span>
|
||||
</p>
|
||||
</br>
|
||||
<p>
|
||||
Funds sent to this address will not be tracked as they are a direct,
|
||||
p2p transaction on the anonymous, private Wownero network.
|
||||
Nobody will have visibility into these and will not be shown.
|
||||
</p>
|
||||
</br>
|
||||
<a href="https://wownero.org/#wallets" target="_blank">Wownero Wallets</a>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tipSCModal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Tip via Smart Contract</p>
|
||||
<button class="delete" aria-label="close" id="closeSCModal"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<p>
|
||||
Sending a tip via the <a href="{{ config.CONTRACT_ADDRESS | show_snowtrace }}" target="_blank">tubbymemes smart contract</a> will send a small amount to the
|
||||
publisher (the address that minted the meme) and the contract owner ({{ config.CONTRACT_OWNER }}), and the remainder is sent to the meme creator (whoever uploaded it). Everyone gets tipped for their contribution, but primarily the meme creator.
|
||||
</p>
|
||||
</br>
|
||||
<p>
|
||||
Input the amount of WOWX or AVAX you would like to send and click a button below to sign the transaction in MetaMask wallet.
|
||||
</p>
|
||||
</br>
|
||||
<div class="field">
|
||||
<label class="label">Tip Amount</label>
|
||||
<div class="control">
|
||||
<input id="tipSCAmount" class="input" type="text" placeholder=".042069">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button id="tipWOWXButton" class="button is-success" onclick="tipWOWX()">Tip WOWX</button>
|
||||
<button id="tipAVAXButton" class="button" onclick="tipAVAX()">Tip AVAX</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{% include 'includes/footer.html' %}
|
||||
{% if meme.approved %}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
// Show buttons depending on if minted or not
|
||||
let minted = await isMinted('{{ meme.meta_ipfs_hash }}');
|
||||
if (minted) {
|
||||
document.getElementById('tipButtons').style.display = "flex";
|
||||
} else {
|
||||
document.getElementById('mintButtons').style.display = "flex";
|
||||
}
|
||||
|
||||
// Show tip amounts
|
||||
try {
|
||||
const tokenId = await getTokenID('{{ meme.meta_ipfs_hash }}');
|
||||
const memeDeets = await contract.methods.tokenMeme(tokenId).call();
|
||||
document.getElementById('tokenId').innerHTML = tokenId;
|
||||
document.getElementById('wowxTips').innerHTML = await w3.utils.fromWei(memeDeets.creatorTipsWOWX);
|
||||
document.getElementById('avaxTips').innerHTML = await w3.utils.fromWei(memeDeets.creatorTipsAVAX);
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
async function tipWOW() {
|
||||
document.getElementById('tipWOWButton').classList.add('is-loading');
|
||||
document.getElementById('tipWOWModal').classList.add('is-active');
|
||||
document.getElementById('closeWOWModal').onclick = () => {
|
||||
document.getElementById('tipWOWModal').classList.remove('is-active');
|
||||
document.getElementById('tipWOWButton').classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function tipSC() {
|
||||
document.getElementById('tipSCButton').classList.add('is-loading');
|
||||
document.getElementById('tipSCModal').classList.add('is-active');
|
||||
document.getElementById('closeSCModal').onclick = () => {
|
||||
document.getElementById('tipSCModal').classList.remove('is-active');
|
||||
document.getElementById('tipSCButton').classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTipAmount() {
|
||||
const amt = document.getElementById('tipSCAmount').value;
|
||||
if (isNaN(amt)) {
|
||||
notif('You must provide a number.', 'warning');
|
||||
return false;
|
||||
} else {
|
||||
return amt;
|
||||
}
|
||||
}
|
||||
|
||||
async function tipAVAX() {
|
||||
document.getElementById('tipAVAXButton').classList.add('is-loading');
|
||||
const tokenId = await getTokenID('{{ meme.meta_ipfs_hash }}');
|
||||
const walletAddress = await getMetamaskAccount();
|
||||
const tipAmount = await getTipAmount();
|
||||
if (!tipAmount) {
|
||||
document.getElementById('tipAVAXButton').classList.remove('is-loading');
|
||||
return false;
|
||||
}
|
||||
const tipAmountWei = w3.utils.toWei(tipAmount);
|
||||
const gasPrice = await w3.eth.getGasPrice();
|
||||
try {
|
||||
const gasLimit = await contract.methods.tipAVAX(tokenId, tipAmountWei).estimateGas({from: walletAddress, value: tipAmountWei}, function(err, gas){
|
||||
return gas;
|
||||
});
|
||||
notif(`Tipping ${tipAmount} AVAX to meme "{{ meme.id | shorten_address }}" by "{{ meme.user.handle }} ({{ meme.user.public_address | shorten_address }})" via the Avalanche blockchain.`, 'info');
|
||||
let res = await contract.methods.tipAVAX(tokenId, tipAmountWei).send({
|
||||
from: walletAddress,
|
||||
value: tipAmountWei,
|
||||
gasPrice: gasPrice,
|
||||
gas: gasLimit
|
||||
});
|
||||
console.log(res);
|
||||
window.location.href = "";
|
||||
} catch(e) {
|
||||
notif(e.message, 'warning');
|
||||
document.getElementById('tipAVAXButton').classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function approveWOWX(amountWei) {
|
||||
const walletAddress = await getMetamaskAccount();
|
||||
const allowanceWei = await wowxContract.methods.allowance(walletAddress, contractAddress).call();
|
||||
if (amountWei <= allowanceWei) {
|
||||
return true;
|
||||
}
|
||||
const gasPrice = await w3.eth.getGasPrice();
|
||||
const gasLimit = await wowxContract.methods.approve(contractAddress, amountWei).estimateGas({from: walletAddress}, function(err, gas){
|
||||
return gas;
|
||||
});
|
||||
try {
|
||||
notif(`Approving usage of WOWX token.`, 'info');
|
||||
let res = await wowxContract.methods.approve(contractAddress, amountWei).send({
|
||||
from: walletAddress,
|
||||
gasPrice: gasPrice,
|
||||
gas: gasLimit
|
||||
});
|
||||
console.log(res);
|
||||
return true;
|
||||
} catch(e) {
|
||||
notif(e.message, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
async function tipWOWX() {
|
||||
document.getElementById('tipWOWXButton').classList.add('is-loading');
|
||||
const tipAmount = await getTipAmount();
|
||||
if (!tipAmount) {
|
||||
document.getElementById('tipWOWXButton').classList.remove('is-loading');
|
||||
return false;
|
||||
}
|
||||
const tipAmountWei = w3.utils.toWei(tipAmount);
|
||||
const tokenId = await getTokenID('{{ meme.meta_ipfs_hash }}');
|
||||
const walletAddress = await getMetamaskAccount();
|
||||
const approved = await approveWOWX(tipAmountWei);
|
||||
if (!approved) {
|
||||
document.getElementById('tipWOWXButton').classList.remove('is-loading');
|
||||
return false;
|
||||
}
|
||||
const gasPrice = await w3.eth.getGasPrice();
|
||||
try {
|
||||
const gasLimit = await contract.methods.tipWOWX(tokenId, tipAmountWei).estimateGas({from: walletAddress}, function(err, gas){
|
||||
return gas;
|
||||
});
|
||||
notif(`Tipping ${tipAmount} WOWX to meme "{{ meme.id | shorten_address }}" by "{{ meme.user.handle }} ({{ meme.user.public_address | shorten_address }})".`, 'info');
|
||||
let res = await contract.methods.tipWOWX(tokenId, tipAmountWei).send({
|
||||
from: walletAddress,
|
||||
gasPrice: gasPrice,
|
||||
gas: gasLimit
|
||||
});
|
||||
console.log(res);
|
||||
window.location.href = "";
|
||||
} catch(e) {
|
||||
notif(e.message, 'warning');
|
||||
document.getElementById('tipWOWXButton').classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function mint() {
|
||||
if (await isMinted('{{ meme.meta_ipfs_hash }}')) {
|
||||
notif('Already minted!', 'warning');
|
||||
return
|
||||
}
|
||||
document.getElementById('mintButton').classList.add('is-loading');
|
||||
const walletAddress = await getMetamaskAccount();
|
||||
const gasPrice = await w3.eth.getGasPrice();
|
||||
const gasLimit = await contract.methods.mint("{{ meme.meta_ipfs_hash }}", "{{ meme.user.public_address }}").estimateGas({from: walletAddress}, function(err, gas){
|
||||
return gas;
|
||||
});
|
||||
try {
|
||||
notif(`Minting meme "{{ meme.id | shorten_address }}" by "{{ meme.user.handle }} ({{ meme.user.public_address | shorten_address }})" to the Avalanche blockchain.`, 'info');
|
||||
let res = await contract.methods.mint("{{ meme.meta_ipfs_hash }}", "{{ meme.user.public_address }}").send({
|
||||
from: walletAddress,
|
||||
value: 0,
|
||||
gasPrice: gasPrice,
|
||||
gas: gasLimit
|
||||
});
|
||||
console.log(res);
|
||||
window.location.href = "";
|
||||
} catch(e) {
|
||||
notif(e.message, 'warning');
|
||||
document.getElementById('mintButton').classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,236 @@
|
||||
{% set is_user = current_user == user %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{% include 'includes/head.html' %}
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
|
||||
{% include 'includes/navbar.html' %}
|
||||
|
||||
{% if user %}
|
||||
<div class="screen">
|
||||
<div class="columns">
|
||||
<div class="column is-full content">
|
||||
<h3>From Avalanche Blockchain</h3>
|
||||
<p>Handle: <strong><span id="userHandle" class="mr-4">?</span></strong></p>
|
||||
<p>Wownero Address: <strong><span id="wowneroAddress" class="mr-4 wrap">?</span></strong></p>
|
||||
<p>Profile IPFS Hash: <strong><span id="metadataIPFSHash">?</span></strong></p>
|
||||
<p>Tipped AVAX: <strong><span id="tippedAVAX">?</span></strong></p>
|
||||
<p>Tipped WOWX: <strong><span id="tippedWOWX">?</span></strong></p>
|
||||
<p>Tipped WOW: <strong><span id="tippedWOW">?</span></strong></p>
|
||||
|
||||
<h3>From Local Database</h3>
|
||||
{# Handle #}
|
||||
{% if is_user %}
|
||||
<div class="field">
|
||||
<label class="label">Handle</label>
|
||||
<div class="control columns">
|
||||
<div class="column is-two-thirds">
|
||||
<input id="handleInput" class="input" type="text" placeholder="{{ user.handle }}" value="{{ user.handle }}">
|
||||
<p class="help">The username/handle you want to be referenced by.</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a id="publishHandle" onclick="publishHandle()" class="publishAVAX button">Publish AVAX</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p><strong>Handle:</strong> {{ user.handle }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# Wownero Address #}
|
||||
{% if is_user %}
|
||||
<div class="field">
|
||||
<label class="label">Wownero Address</label>
|
||||
<div class="control columns">
|
||||
<div class="column is-two-thirds">
|
||||
<input id="wowneroAddressInput" class="input" type="text" placeholder="{{ user.wownero_address or '' }}" value="{{ user.wownero_address or '' }}">
|
||||
<p class="help">The Wownero wallet address you want to receive tips on.</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a id="publishWowneroAddress" onclick="publishWowneroAddress()" class="publishAVAX button">Publish AVAX</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p><strong>Wownero Address:</strong> <span class="wrap">{{ user.wownero_address or '?' }}</span></p>
|
||||
{% endif %}
|
||||
|
||||
{# Metadata IPFS Hash #}
|
||||
{% if is_user %}
|
||||
<div class="field">
|
||||
<label class="label">Metadata IPFS Hash</label>
|
||||
<div class="control columns">
|
||||
<div class="column is-two-thirds">
|
||||
<input id="metadataIPFSHashInput" class="input" type="text" placeholder="{{ user.ipfs_hash or '' }}" value="{{ user.ipfs_hash or '' }}">
|
||||
<p class="help">The IPFS hash of your profile metadata.</p>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a id="publishMetadataIPFSHash" onclick="publishMetadataIPFSHash()" class="publishAVAX button">Publish AVAX</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p><strong>Metadata IPFS Hash:</strong> {{ user.ipfs_hash or '' }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p><strong>Register Date: </strong> {{ user.register_date }} ({{ user.register_date | humanize }})</p>
|
||||
<p><strong>Last Login Date: </strong> {{ user.last_login_date }} ({{ user.last_login_date | humanize }})</p>
|
||||
<p><strong>Moderator: </strong> {{ user.is_moderator() }}</p>
|
||||
<p><strong>Verified: </strong> {{ user.verified }}</p>
|
||||
<p><strong>Memes Posted: </strong> {{ user.memes | length }}</p>
|
||||
{% if user.bio %}
|
||||
<p><strong>Bio: </strong> {{ user.bio }}</p>
|
||||
{% endif %}
|
||||
{% if user.website %}
|
||||
<p>Website: <a href="{{ user.website_url }}" target="_blank">{{ user.website_url }}</a></p>
|
||||
{% endif %}
|
||||
{% if is_user %}
|
||||
<p><a onclick="saveDB()" class="ml-2 button is-primary">Save DB</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{% include 'includes/footer.html' %}
|
||||
|
||||
<script type="text/javascript">
|
||||
async function saveDB() {
|
||||
await fetch('{{ url_for("api.update_user" ) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'user_id': {{ user.id }},
|
||||
'handle': document.getElementById('handleInput').value,
|
||||
'wownero_address': document.getElementById('wowneroAddressInput').value,
|
||||
'ipfs_hash': document.getElementById('metadataIPFSHashInput').value,
|
||||
})
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then(function(data) {
|
||||
console.log(data)
|
||||
if (data['success']) {
|
||||
notif('Updated user information in this server\'s database.', 'success');
|
||||
notif('Publish to the Avalanche blockchain to make the data globally available.', 'info');
|
||||
} else {
|
||||
notif(data['message'], 'error');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async function fetchUserProfile() {
|
||||
await addAvalancheNetwork();
|
||||
_walletAddress = w3.utils.toChecksumAddress('{{ user.public_address }}');
|
||||
const userProfile = await contract.methods.userProfile(_walletAddress).call();
|
||||
if (userProfile.userHandle){document.getElementById('userHandle').innerHTML = userProfile.userHandle};
|
||||
if (userProfile.wowneroAddress){document.getElementById('wowneroAddress').innerHTML = userProfile.wowneroAddress};
|
||||
if (userProfile.metadataIPFSHash){document.getElementById('metadataIPFSHash').innerHTML = userProfile.metadataIPFSHash};
|
||||
if (userProfile.tippedAVAX){document.getElementById('tippedAVAX').innerHTML = w3.utils.fromWei(userProfile.tippedAVAX)};
|
||||
if (userProfile.tippedWOWX){document.getElementById('tippedWOWX').innerHTML = w3.utils.fromWei(userProfile.tippedWOWX)};
|
||||
return
|
||||
}
|
||||
|
||||
async function publishHandle() {
|
||||
document.getElementById('publishHandle').classList.add('is-loading');
|
||||
const handle = document.getElementById('handleInput').value;
|
||||
if (handle == "") {
|
||||
notif('Cannot publish an empty value.', 'warning');
|
||||
document.getElementById('publishHandle').classList.remove('is-loading');
|
||||
return false;
|
||||
}
|
||||
await addAvalancheNetwork();
|
||||
try {
|
||||
const walletAddress = await getMetamaskAccount();
|
||||
const gasPrice = await w3.eth.getGasPrice();
|
||||
const gasLimit = await contract.methods.setUserHandle(handle).estimateGas(function(err, gas){
|
||||
return gas;
|
||||
});
|
||||
notif(`Publishing user handle "${handle}" to the Avalanche blockchain for address ${walletAddress.slice(0, 6)}...${walletAddress.slice(-6)}.`, 'info');
|
||||
let res = await contract.methods.setUserHandle(handle).send({
|
||||
from: walletAddress,
|
||||
value: 0,
|
||||
gasPrice: gasPrice,
|
||||
gas: gasLimit
|
||||
});
|
||||
console.log(res);
|
||||
window.location.href = "";
|
||||
} catch(e) {
|
||||
notif(e.message, 'warning');
|
||||
document.getElementById('publishHandle').classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function publishWowneroAddress() {
|
||||
document.getElementById('publishWowneroAddress').classList.add('is-loading');
|
||||
const address = document.getElementById('wowneroAddressInput').value;
|
||||
if (address == "") {
|
||||
notif('Cannot publish an empty value.', 'warning');
|
||||
document.getElementById('publishWowneroAddress').classList.remove('is-loading');
|
||||
return false;
|
||||
}
|
||||
await addAvalancheNetwork();
|
||||
try {
|
||||
const walletAddress = await getMetamaskAccount();
|
||||
const gasPrice = await w3.eth.getGasPrice();
|
||||
const gasLimit = await contract.methods.setUserWowneroAddress(address).estimateGas(function(err, gas){
|
||||
return gas;
|
||||
});
|
||||
notif(`Publishing Wownero address "${address.slice(0, 6)}...${address.slice(-6)}" to the Avalanche blockchain for address ${walletAddress.slice(0, 6)}...${walletAddress.slice(-6)}.`, 'info');
|
||||
let res = await contract.methods.setUserWowneroAddress(address).send({
|
||||
from: walletAddress,
|
||||
value: 0,
|
||||
gasPrice: gasPrice,
|
||||
gas: gasLimit
|
||||
});
|
||||
console.log(res);
|
||||
window.location.href = "";
|
||||
} catch(e) {
|
||||
notif(e.message, 'warning');
|
||||
document.getElementById('publishWowneroAddress').classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function publishMetadataIPFSHash() {
|
||||
document.getElementById('publishMetadataIPFSHash').classList.add('is-loading');
|
||||
const _hash = document.getElementById('metadataIPFSHashInput').value;
|
||||
if (_hash == "") {
|
||||
notif('Cannot publish an empty value.', 'warning');
|
||||
document.getElementById('publishMetadataIPFSHash').classList.remove('is-loading');
|
||||
return false;
|
||||
}
|
||||
await addAvalancheNetwork();
|
||||
try {
|
||||
const walletAddress = await getMetamaskAccount();
|
||||
const gasPrice = await w3.eth.getGasPrice();
|
||||
const gasLimit = await contract.methods.setUserMetadata(_hash).estimateGas(function(err, gas){
|
||||
return gas;
|
||||
});
|
||||
notif(`Publishing user profile metadata IPFS hash "${_hash}" to the Avalanche blockchain for address ${walletAddress.slice(0, 6)}...${walletAddress.slice(-6)}.`, 'info');
|
||||
let res = await contract.methods.setUserMetadata(_hash).send({
|
||||
from: walletAddress,
|
||||
value: 0,
|
||||
gasPrice: gasPrice,
|
||||
gas: gasLimit
|
||||
});
|
||||
console.log(res);
|
||||
window.location.href = "";
|
||||
} catch(e) {
|
||||
notif(e.message, 'warning');
|
||||
document.getElementById('publishMetadataIPFSHash').classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
fetchUserProfile();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{% include 'includes/head.html' %}
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% include 'includes/navbar.html' %}
|
||||
|
||||
<div class="screen">
|
||||
<form method="POST" enctype="multipart/form-data" class="site-form" id="memeUpload" action="{{ url_for('meme.publish') }}" style="padding-top:1.5em;">
|
||||
<div class="field">
|
||||
<label class="label">File</label>
|
||||
<div class="control">
|
||||
<input class="filestyle" id="file" name="file" required type="file">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Poster Address</label>
|
||||
<div class="control">
|
||||
<p>{{ current_user.public_address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Title</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Title of your meme" name="title">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="Description..." name="description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox">
|
||||
I agree to support the ongoing funding of exploratory meme missions
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-primary" up-follow=".container">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{% include 'includes/head.html' %}
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% include 'includes/navbar.html' %}
|
||||
|
||||
<div class="screen">
|
||||
<form method="POST" enctype="multipart/form-data" class="site-form" action="{{ url_for('meta.remotes') }}" style="padding-top:1.5em;">
|
||||
<div class="field">
|
||||
<label class="label">Remote Server</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="https://tubbymemes.xyz" name="endpoint">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-primary">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% if remotes %}
|
||||
<hr>
|
||||
{% for remote in remotes %}
|
||||
<p>{{ remote.endpoint }} - last synced: {{ remote.last_sync_date | humanize }} <a href="{{ url_for('meta.delete_remote', remote_id=remote.id) }}">x</a></p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% include 'includes/footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user