settings: add new settings pages to API

settings: remove old settings pages from API
settings: delete unused settings template
master
justcool393 2022-11-06 01:02:15 -06:00
parent 6e0fd23ba3
commit 511ed26574
5 changed files with 73 additions and 798 deletions

View File

@ -15,7 +15,17 @@ import os
from files.helpers.sanitize import filter_emojis_only
from shutil import copyfile
import requests
import tldextract
@app.get("/settings")
@auth_required
def settings(v):
return redirect("/settings/personal")
@app.get("/settings/personal")
@auth_required
def settings_personal(v):
return render_template("settings_personal.html", v=v)
@app.post("/settings/removebackground")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@ -116,25 +126,25 @@ def settings_profile_post(v):
v.bio = None
v.bio_html = None
g.db.add(v)
return render_template("settings_profile.html", v=v, msg="Your bio has been updated.")
return render_template("settings_personal.html", v=v, msg="Your bio has been updated.")
elif request.values.get("sig") == "":
v.sig = None
v.sig_html = None
g.db.add(v)
return render_template("settings_profile.html", v=v, msg="Your sig has been updated.")
return render_template("settings_personal.html", v=v, msg="Your sig has been updated.")
elif request.values.get("friends") == "":
v.friends = None
v.friends_html = None
g.db.add(v)
return render_template("settings_profile.html", v=v, msg="Your friends list has been updated.")
return render_template("settings_personal.html", v=v, msg="Your friends list has been updated.")
elif request.values.get("enemies") == "":
v.enemies = None
v.enemies_html = None
g.db.add(v)
return render_template("settings_profile.html", v=v, msg="Your enemies list has been updated.")
return render_template("settings_personal.html", v=v, msg="Your enemies list has been updated.")
elif v.patron and request.values.get("sig"):
sig = request.values.get("sig")[:200].replace('\n','').replace('\r','')
@ -142,14 +152,14 @@ def settings_profile_post(v):
sig_html = sanitize(sig)
if len(sig_html) > 1000:
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
error="Your sig is too long")
v.sig = sig[:200]
v.sig_html=sig_html
g.db.add(v)
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
msg="Your sig has been updated.")
@ -162,7 +172,7 @@ def settings_profile_post(v):
friends_html = sanitize(friends)
if len(friends_html) > 2000:
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
error="Your friends list is too long")
@ -177,7 +187,7 @@ def settings_profile_post(v):
v.friends = friends[:500]
v.friends_html=friends_html
g.db.add(v)
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
msg="Your friends list has been updated.")
@ -188,7 +198,7 @@ def settings_profile_post(v):
enemies_html = sanitize(enemies)
if len(enemies_html) > 2000:
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
error="Your enemies list is too long")
@ -203,7 +213,7 @@ def settings_profile_post(v):
v.enemies = enemies[:500]
v.enemies_html=enemies_html
g.db.add(v)
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
msg="Your enemies list has been updated.")
@ -219,7 +229,7 @@ def settings_profile_post(v):
bio_html = sanitize(bio)
if len(bio_html) > 10000:
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
error="Your bio is too long")
@ -228,7 +238,7 @@ def settings_profile_post(v):
v.bio = bio[:1500]
v.bio_html=bio_html
g.db.add(v)
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
msg="Your bio has been updated.")
@ -325,7 +335,7 @@ def namecolor(v):
if color.startswith('#'): color = color[1:]
if not color_regex.fullmatch(color):
return render_template("settings_profile.html", v=v, error="Invalid color hex code")
return render_template("settings_personal.html", v=v, error="Invalid color hex code")
v.namecolor = color
g.db.add(v)
@ -341,7 +351,7 @@ def themecolor(v):
if themecolor.startswith('#'): themecolor = themecolor[1:]
if not color_regex.fullmatch(themecolor):
return render_template("settings_profile.html", v=v, error="Invalid color hex code")
return render_template("settings_personal.html", v=v, error="Invalid color hex code")
v.themecolor = themecolor
g.db.add(v)
@ -393,7 +403,7 @@ def titlecolor(v):
if titlecolor.startswith('#'): titlecolor = titlecolor[1:]
if not color_regex.fullmatch(titlecolor):
return render_template("settings_profile.html", v=v, error="Invalid color hex code")
return render_template("settings_personal.html", v=v, error="Invalid color hex code")
v.titlecolor = titlecolor
g.db.add(v)
return redirect("/settings/profile")
@ -405,7 +415,7 @@ def titlecolor(v):
def verifiedcolor(v):
verifiedcolor = str(request.values.get("verifiedcolor", "")).strip()
if verifiedcolor.startswith('#'): verifiedcolor = verifiedcolor[1:]
if len(verifiedcolor) != 6: return render_template("settings_profile.html", v=v, error="Invalid color hex code")
if len(verifiedcolor) != 6: return render_template("settings_personal.html", v=v, error="Invalid color hex code")
v.verifiedcolor = verifiedcolor
g.db.add(v)
return redirect("/settings/profile")
@ -544,7 +554,7 @@ def settings_images_profile(v):
g.db.add(v)
return render_template("settings_profile.html", v=v, msg="Profile picture successfully updated.")
return render_template("settings_personal.html", v=v, msg="Profile picture successfully updated.")
@app.post("/settings/images/banner")
@ -568,19 +578,11 @@ def settings_images_banner(v):
v.bannerurl = bannerurl
g.db.add(v)
return render_template("settings_profile.html", v=v, msg="Banner successfully updated.")
@app.get("/settings/blocks")
@auth_required
def settings_blockedpage(v):
return render_template("settings_blocks.html", v=v)
return render_template("settings_personal.html", v=v, msg="Banner successfully updated.")
@app.get("/settings/css")
@auth_required
def settings_css_get(v):
return render_template("settings_css.html", v=v)
@app.post("/settings/css")
@ -600,11 +602,6 @@ def settings_css(v):
return render_template("settings_css.html", v=v)
@app.get("/settings/profilecss")
@auth_required
def settings_profilecss_get(v):
return render_template("settings_profilecss.html", v=v)
@app.post("/settings/profilecss")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{SITE}-{session.get("lo_user")}')
@ -614,11 +611,20 @@ def settings_profilecss(v):
valid, error = validate_css(profilecss)
if not valid:
return render_template("settings_profilecss.html", error=error, v=v)
return render_template("settings_css.html", error=error, v=v)
v.profilecss = profilecss
g.db.add(v)
return render_template("settings_profilecss.html", v=v)
return render_template("settings_css.html", v=v)
@app.get("/settings/security")
@auth_required
def settings_security(v):
return render_template("settings_security.html",
v=v,
mfa_secret=pyotp.random_base32() if not v.mfa_secret else None,
now=int(time.time())
)
@app.post("/settings/block")
@limiter.limit("1/second;20/day")
@ -626,7 +632,6 @@ def settings_profilecss(v):
@auth_required
def settings_block_user(v):
user = get_user(request.values.get("username"), graceful=True)
if not user: abort(404, "This user doesn't exist.")
if user.unblockable:
@ -638,17 +643,13 @@ def settings_block_user(v):
if user.id == AUTOJANNY_ID: abort(403, "You can't block this user")
if v.has_blocked(user): abort(409, f"You have already blocked @{user.username}")
new_block = UserBlock(user_id=v.id,
target_id=user.id,
)
new_block = UserBlock(user_id=v.id, target_id=user.id)
g.db.add(new_block)
if user.admin_level >= PERMS['USER_BLOCKS_VISIBLE']:
send_notification(user.id, f"@{v.username} has blocked you!")
cache.delete_memoized(frontlist)
return {"message": f"@{user.username} blocked."}
@ -661,27 +662,20 @@ def settings_unblock_user(v):
x = v.has_blocked(user)
if not x: abort(409, "You can't unblock someone you haven't blocked")
g.db.delete(x)
if not v.shadowbanned and user.admin_level >= PERMS['USER_BLOCKS_VISIBLE']:
send_notification(user.id, f"@{v.username} has unblocked you!")
cache.delete_memoized(frontlist)
return {"message": f"@{user.username} unblocked."}
@app.get("/settings/apps")
@auth_required
def settings_apps(v):
return render_template("settings_apps.html", v=v)
@app.get("/settings/content")
@app.get("/settings/advanced")
@auth_required
def settings_content_get(v):
return render_template("settings_filters.html", v=v)
def settings_advanced_get(v):
return render_template("settings_advanced.html", v=v)
@app.post("/settings/name_change")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@ -691,12 +685,12 @@ def settings_name_change(v):
new_name=request.values.get("name").strip()
if new_name==v.username:
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
error="You didn't change anything")
if not valid_username_regex.fullmatch(new_name):
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
error="This isn't a valid username.")
@ -710,7 +704,7 @@ def settings_name_change(v):
).one_or_none()
if x and x.id != v.id:
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
error=f"Username `{new_name}` is already in use.")
@ -733,7 +727,7 @@ def settings_song_change_mp3(v):
file = request.files['file']
if file.content_type != 'audio/mpeg':
return render_template("settings_profile.html", v=v, error="Not a valid MP3 file")
return render_template("settings_personal.html", v=v, error="Not a valid MP3 file")
song = str(time.time()).replace('.','')
@ -743,7 +737,7 @@ def settings_song_change_mp3(v):
size = os.stat(name).st_size
if size > 8 * 1024 * 1024:
os.remove(name)
return render_template("settings_profile.html", v=v, error="MP3 file must be smaller than 8MB")
return render_template("settings_personal.html", v=v, error="MP3 file must be smaller than 8MB")
if path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User).filter_by(song=v.song).count() == 1:
os.remove(f"/songs/{v.song}.mp3")
@ -778,7 +772,7 @@ def settings_song_change(v):
elif song.startswith("https://youtu.be/"):
id = song.split("https://youtu.be/")[1]
else:
return render_template("settings_profile.html", v=v, error="Not a youtube link.")
return render_template("settings_personal.html", v=v, error="Not a youtube link.")
if "?" in id: id = id.split("?")[0]
if "&" in id: id = id.split("&")[0]
@ -792,15 +786,15 @@ def settings_song_change(v):
req = requests.get(f"https://www.googleapis.com/youtube/v3/videos?id={id}&key={YOUTUBE_KEY}&part=contentDetails", timeout=5).json()
duration = req['items'][0]['contentDetails']['duration']
if duration == 'P0D':
return render_template("settings_profile.html", v=v, error="Can't use a live youtube video!")
return render_template("settings_personal.html", v=v, error="Can't use a live youtube video!")
if "H" in duration:
return render_template("settings_profile.html", v=v, error="Duration of the video must not exceed 15 minutes.")
return render_template("settings_personal.html", v=v, error="Duration of the video must not exceed 15 minutes.")
if "M" in duration:
duration = int(duration.split("PT")[1].split("M")[0])
if duration > 15:
return render_template("settings_profile.html", v=v, error="Duration of the video must not exceed 15 minutes.")
return render_template("settings_personal.html", v=v, error="Duration of the video must not exceed 15 minutes.")
if v.song and path.isfile(f"/songs/{v.song}.mp3") and g.db.query(User).filter_by(song=v.song).count() == 1:
@ -820,7 +814,7 @@ def settings_song_change(v):
try: ydl.download([f"https://youtube.com/watch?v={id}"])
except Exception as e:
print(e, flush=True)
return render_template("settings_profile.html",
return render_template("settings_personal.html",
v=v,
error="Age-restricted videos aren't allowed.")
@ -846,14 +840,14 @@ def settings_title_change(v):
customtitleplain = request.values.get("title").strip().replace("𒐪","")[:100]
if customtitleplain == v.customtitleplain:
return render_template("settings_profile.html", v=v, error="You didn't change anything")
return render_template("settings_personal.html", v=v, error="You didn't change anything")
customtitle = filter_emojis_only(customtitleplain)
customtitle = censor_slurs(customtitle, None)
if len(customtitle) > 1000:
return render_template("settings_profile.html", v=v, error="Flair too long!")
return render_template("settings_personal.html", v=v, error="Flair too long!")
v.customtitleplain = customtitleplain
v.customtitle = customtitle
@ -873,13 +867,13 @@ def settings_pronouns_change(v):
pronouns = request.values.get("pronouns").replace("𒐪","").strip()
if len(pronouns) > 11:
return render_template("settings_profile.html", v=v, error="Your pronouns exceed the character limit (11 characters)")
return render_template("settings_personal.html", v=v, error="Your pronouns exceed the character limit (11 characters)")
if pronouns == v.pronouns:
return render_template("settings_profile.html", v=v, error="You didn't change anything.")
return render_template("settings_personal.html", v=v, error="You didn't change anything.")
if not pronouns_regex.fullmatch(pronouns):
return render_template("settings_profile.html", v=v, error="The pronouns you entered don't match the required format.")
return render_template("settings_personal.html", v=v, error="The pronouns you entered don't match the required format.")
bare_pronouns = pronouns.lower().replace('/', '')
if 'nig' in bare_pronouns: pronouns = 'BI/POC'
@ -896,28 +890,10 @@ def settings_pronouns_change(v):
@limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{SITE}-{session.get("lo_user")}')
@auth_required
def settings_checkmark_text(v):
if not v.verified: abort(403)
new_name=request.values.get("title").strip()[:100].replace("𒐪","")
if not new_name: abort(400)
if new_name == v.verified: return render_template("settings_profile.html", v=v, error="You didn't change anything")
if new_name == v.verified: return render_template("settings_personal.html", v=v, error="You didn't change anything")
v.verified = new_name
g.db.add(v)
return redirect("/settings/profile")
@app.get("/settings")
@auth_required
def settings(v):
return redirect("/settings/profile")
@app.get("/settings/profile")
@auth_required
def settings_profile(v):
return render_template("settings_profile.html", v=v)

View File

@ -360,16 +360,6 @@ def serviceworker():
with open("files/assets/js/service-worker.js", "r", encoding="utf-8") as f:
return Response(f.read(), mimetype='application/javascript')
@app.get("/settings/security")
@auth_required
def settings_security(v):
return render_template("settings_security.html",
v=v,
mfa_secret=pyotp.random_base32() if not v.mfa_secret else None,
now=int(time.time())
)
@app.post("/dismiss_mobile_tip")
def dismiss_mobile_tip():
session["tooltip_last_dismissed"] = int(time.time())

View File

@ -1,11 +1,9 @@
{% extends "settings.html" %}
{% block pagetitle %}<title>Apps/Bots - {{SITE_NAME}}</title>{% endblock %}
{% block pagetitle %}Apps/Bots - {{SITE_NAME}}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
<h5><a href="/api">API Guide</a></h5>
<h5 class="mt-1">Your API Applications</h5>
{% for app in v.applications if app.client_id %}

View File

@ -1,6 +1,18 @@
{% extends "settings.html" %}
{% block pagetitle %}Content Settings - {{SITE_NAME}}{% endblock %}
{% import 'settings_common.html' as common with context %}
<style>
.bg-image {
padding: 0.25rem;
width: 15rem;
height: 10rem;
object-fit: cover;
}
.bg-button {
margin: 0.25rem;
padding: 0;
}
</style>
{# common sections start #}
{% macro color_section(id, form_action, form_name, section_title, current_color) %}
<div class="d-lg-flex border-bottom">

View File

@ -1,701 +0,0 @@
{% extends "settings.html" %}
{% block pagetitle %}Profile Settings - {{SITE_NAME}}{% endblock %}
{% block content %}
<style>
.bg-image {
padding: 0.25rem;
width: 15rem;
height: 10rem;
object-fit: cover;
}
.bg-button {
margin: 0.25rem;
padding: 0;
}
</style>
<div id="posts" class="row">
<div class="col col-lg-10">
<div class="settings">
{% if FEATURES['HOUSES'] %}
<h5 name="referral">House</h5>
<div class="settings-section rounded">
<div class="d-lg-flex border-bottom">
<div class="title w-lg-25">
<label for="theme">House</label>
</div>
<div class="body w-lg-100">
{% if v.house %}
{% set cost = HOUSE_SWITCH_COST %}
<p>Change your house (cost: {{cost}} coins or marseybux).</p>
{% if ' Founder' in v.house %}
<p>Warning: you'll lose your founder status if you join a different house</p>
{% endif %}
{% else %}
{% set cost = HOUSE_JOIN_COST %}
<p>Join a house (cost: {{cost}} coins or marseybux).</p>
{% endif %}
<div class="input-group mb2">
<select {% if v.coins < cost and v.procoins < cost or v.bite %}disabled{% endif %} autocomplete="off" id='house' class="form-control" form="profile-settings" name="house" onchange="postToastReload(this,'/settings/profile?house='+document.getElementById('house').value)">
{% for entry in ("None","Furry","Femboy","Vampire","Racist") %}
<option value="{{entry}}" {% if v.house == entry %} selected {% endif %}>
{{entry}}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
{% endif %}
<h5 name="referral">Theme</h5>
<div class="settings-section rounded">
<div class="d-lg-flex border-bottom">
<div class="title w-lg-25">
<label for="cardview">Card View</label>
</div>
<div class="body w-lg-100">
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="cardview" name="cardview"{% if v.cardview %} checked{% endif %} onchange="postToastSwitch(this,'/settings/profile?cardview='+document.getElementById('cardview').checked);">
<label class="custom-control-label" for="cardview"></label>
</div>
<span class="text-small text-muted">Enable if you would like to display images and videos in full size on the frontpage.</span>
</div>
</div>
<div class="d-lg-flex border-bottom">
<div class="title w-lg-25">
<label for="highlightcomments">Highlight New Comments</label>
</div>
<div class="body w-lg-100">
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="highlightcomments" name="highlightcomments"{% if v.highlightcomments %} checked{% endif %} onchange="postToastSwitch(this,'/settings/profile?highlightcomments='+document.getElementById('highlightcomments').checked);">
<label class="custom-control-label" for="highlightcomments"></label>
</div>
<span class="text-small text-muted">Enable if you would like to highlight comments made after the last time you visited a thread.</span>
</div>
</div>
<div class="d-lg-flex border-bottom">
<div class="title w-lg-25">
<label for="theme">Website Theme</label>
</div>
<div class="body w-lg-100">
<p>Change the theme for the website.</p>
<div class="input-group mb2">
<select autocomplete="off" id='theme' class="form-control" form="profile-settings" name="theme" onchange="postToastReload(this,'/settings/profile?theme='+document.getElementById('theme').value)">
{% set themes = ["4chan","classic","classic_dark","coffee","dark","dramblr","light","midnight","transparent","tron","win98"] %}
{% for entry in themes %}
<option value="{{entry}}" {% if v.theme==entry %} selected {% endif %}>
{{entry}}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Theme Color</label>
<div class="d-flex">
<form action="/settings/themecolor" id="themecolor-form" method="post" class="color-picker" style="line-height: 0">
<input type="hidden" name="formkey" value="{{v.formkey}}">
{% for themecolor in COLORS %}
<input autocomplete="off" type="radio" name="themecolor" id="themecolor-{{themecolor}}" value="{{themecolor}}" {% if v.themecolor == themecolor %}checked{% endif %} onclick="document.getElementById('themecolor-form').submit()">
<label class="color-radio" for="themecolor-{{themecolor}}">
<span style="background-color: #{{themecolor}}">
{% if v.themecolor.lower() == themecolor %}
<i class="fas fa-check text-white"></i>
{% else %}
&nbsp;
{% endif %}
</span>
</label>
{% endfor %}
</form>
</div>
<p class="text-small mb-2">Or type a color hex code:</p>
<div class="d-flex">
<form action="/settings/themecolor" id="color-code-form" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input class="form-control" type="text" name="themecolor" id="color-code" minlength="6" maxlength="6" value="{% if v.themecolor %}{{v.themecolor}}{% endif %}">
<label class="btn btn-secondary text-capitalize mr-2 mt-2 mb-0">Update<input type="text" for="color-code" onclick="form.submit()" hidden=""></label>
</form>
</div>
</div>
<div class="d-lg-flex border-bottom">
<div class="title w-lg-25">
<label for="theme">Website Backgrounds</label>
</div>
<div class="body w-lg-100">
<p>Change the background for the website.</p>
<div class="input-group mb2">
<select autocomplete="off" id='backgroundSelector' class="form-control" form="profile-settings" name="background" onchange="updatebgselection();">
{% for entry in ["anime", "fantasy", "solarpunk", "pixelart"] %}
<option value="{{entry}}" {% if v.background and v.background.startswith(entry) %}selected{% endif %}>
{{entry}}
</option>
{% endfor %}
</select>
</div>
{% if v.background %}
<div class="d-flex mt-2">
<button type="button" class="btn btn-primary ml-auto mb-1" onclick="postToastReload(this,'/settings/removebackground')">Remove Background</button>
</div>
{% endif %}
<div id="bgcontainer"></div>
</div>
</div>
</div>
<h5>Profile Picture</h5>
<div class="settings-section rounded">
<div class="d-flex">
<div class="title w-lg-25 text-md-center">
<img loading="lazy" alt="your profile picture" src="{{v.profile_url}}" class="profile-pic-75">
</div>
<div class="body w-lg-100 my-auto">
<div class="d-flex">
<div>
<form action="/settings/images/profile" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label class="btn btn-secondary text-capitalize mr-2 mb-0">
Update<input autocomplete="off" type="file" accept="image/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} hidden name="profile" onchange="form.submit()">
</label>
</form>
</div>
</div>
<div class="text-small text-muted mt-3">All image files are supported. Max file size is {% if v and v.patron %}16{% else %}8{% endif %} MB.</div>
</div>
</div>
</div>
{% if FEATURES['USERS_PROFILE_BANNER'] -%}
<h5>Profile Banner</h5>
<div class="settings-section rounded">
<div class="d-flex">
<div class="title w-lg-75 text-md-center">
<img loading="lazy" alt="your banner" src="{{v.banner_url}}" class="banner-pic-135">
</div>
<div class="body w-lg-100 my-auto">
<div class="d-flex">
<div>
<form action="/settings/images/banner" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label class="btn btn-secondary text-capitalize mr-2 mb-0">
Update<input autocomplete="off" type="file" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} accept="image/*" hidden name="banner" onchange="form.submit()">
</label>
</form>
</div>
</div>
<div class="text-small text-muted mt-3">All image files are supported. Max file size is {% if v and v.patron %}16{% else %}8{% endif %} MB.</div>
</div>
</div>
</div>
{%- endif %}
<h5 id="referral" name="referral">Referrals</h5>
<p class="text-small text-muted">Invite a friend.</p>
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="referral_code">Referral code</label>
</div>
<div class="body w-lg-100">
<div class="input-group">
<input autocomplete="off" type="text" readonly="" class="form-control copy-link" id="referral_code" value="{{SITE_FULL}}/signup?ref={{v.username}}" data-clipboard-text="{{SITE_FULL}}/signup?ref={{v.username}}">
<span class="input-group-append" data-bs-toggle="tooltip" data-bs-placement="top" title="You have referred {{v.referral_count}} user{{'s' if v.referral_count != 1 else ''}} so far. {% if v.referral_count==0 %}¯\_(ツ)_/¯{% elif v.referral_count>10%}Wow!{% endif %}">
<span class="input-group-text text-primary border-0">
<i class="far fa-user mr-1" aria-hidden="true"></i>{{v.referral_count}}</span>
</span>
</div>
<div class="text-small text-muted mt-3">Share this link with a friend. {% if v.referral_count==0 %} When they sign up, you'll get the bronze recruitment badge. <a href="/badges">Learn more.</a>{% elif v.referral_count<10 %} When you refer 10 friends, you'll receive the silver recruitment badge. <a href="/badges">Learn more.</a>{% elif v.referral_count<100 %} When you refer 100 friends, you'll receive the gold recruitment badge. <a href="/badges">Learn more</a>.{% endif %}</div>
</div>
</div>
</div>
<h5 name="referral">RSS Feed</h5>
<p class="text-small text-muted">Subscribe to the {{SITE_NAME}} RSS feed.</p>
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="body w-lg-100">
<input autocomplete="off" type="text" readonly="" class="form-control copy-link" value="{{SITE_FULL}}/rss/hot/all" data-clipboard-text="{{SITE_FULL}}/rss/hot/all">
<div class="text-small text-muted mt-3">You can change the feed by replacing "hot" with whatever sorting you want and "all" with whatever time filter you want.</div>
</div>
</div>
</div>
<h5 id="bio" name="bio">Your Profile</h5>
<p class="text-small text-muted">Edit how others see you on {{SITE_NAME}}.</p>
<div class="settings-section rounded mb-0">
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Username</label>
<div class="w-lg-100">
<p>Your original username will always stay reserved for you: <code>{{v.original_username}}</code></p>
<form action="/settings/name_change" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input autocomplete="off" type="text" name="name" class="form-control" value="{{v.username}}">
<small>3-25 characters, including letters, numbers, _ , and -</small>
<div class="d-flex mt-2">
<input autocomplete="off" class="btn btn-primary ml-auto" type="submit" onclick="disable(this)" value="Change Display Name">
</div>
</form>
</div>
</div>
{% if FEATURES['USERS_PROFILE_SONG'] -%}
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Profile Anthem</label>
<div class="w-lg-100">
<p>You can use an MP3 file or a YouTube video.</p>
<form action="/settings/song_change_mp3" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label class="btn btn-secondary format d-inline-block m-0 mb-3">
<div id="filename-show2"><i class="fas fa-file"></i>
{% if v.song and v.song|length in (1,2,3,4,5,17) %}
{{v.song}}.mp3
{% else %}
Use an MP3 file (Max size is 8MB)
{% endif %}
</div>
<input autocomplete="off" id="file-upload2" type="file" name="file" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} accept="audio/mp3" onchange="this.form.submit()" hidden>
</label>
</form>
<form action="/settings/song_change" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input class="form-control" style="display:inline;max-width:75%;font-size: min(3.5vw,16px)!important" autocomplete="off" type="text" name="song" class="form-control" value="{% if v.song and v.song|length not in (1,2,3,4,5,17) %}https://youtu.be/{{v.song}}{% endif %}" placeholder='Enter a YouTube video link here'>
<input class="btn btn-primary" style="font-size: min(3.5vw,16px)!important" autocomplete="off" class="btn btn-primary ml-auto" type="submit" onclick="disable(this)" value="Submit">
</form>
<br><small>In some browsers, users have to click at least once anywhere in the profile page for the anthem to play.</small>
</div>
</div>
{%- endif %}
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Name Color</label>
<div class="d-flex">
<form action="/settings/namecolor" id="color-form" method="post" class="color-picker" style="line-height: 0">
<input type="hidden" name="formkey" value="{{v.formkey}}">
{% for color in COLORS %}
<input autocomplete="off" type="radio" name="color" id="color-{{color}}" value="{{color}}" {% if v.name_color == color %}checked{% endif %} onclick="document.getElementById('color-form').submit()">
<label class="color-radio" for="color-{{color}}">
<span style="background-color: #{{color}}">
{% if v.name_color.lower() == color %}
<i class="fas fa-check text-white"></i>
{% else %}
&nbsp;
{% endif %}
</span>
</label>
{% endfor %}
</form>
</div>
<p class="text-small mb-2">Or type a color hex code:</p>
<div class="d-flex">
<form action="/settings/namecolor" id="color-code-form" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input autocomplete="off" class="form-control" type="text" name="color" id="color-code" minlength="6" maxlength="6" value="{% if v.name_color %}{{v.name_color}}{% endif %}">
<label class="btn btn-secondary text-capitalize mr-2 mt-2 mb-0">Update<input autocomplete="off" type="text" for="color-code" onclick="form.submit()" hidden=""></label>
</form>
</div>
</div>
{% if FEATURES['PRONOUNS'] %}
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Pronouns</label>
<div class="w-lg-100">
<form id="profile-settings" action="/settings/pronouns_change" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input minlength=3 maxlength=11 pattern="([a-zA-Z]{1,5})/[a-zA-Z]{1,5}(/[a-zA-Z]{1,5})?" autocomplete="off" id="pronounbody" type="text" name="pronouns" class="form-control" placeholder='Enter pronouns here' value="{% if v.pronouns %}{{v.pronouns}}{% endif %}">
<div class="d-flex mt-2">
<small>{2-5 characters} / {2-5 characters}</small>
<input autocomplete="off" class="btn btn-primary ml-auto" id="pronounsSave" type="submit" onclick="disable(this)" value="Change Pronouns">
</div>
</form>
</div>
</div>
{% endif %}
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Flair</label>
<div class="w-lg-100">
<form id="profile-settings" action="/settings/title_change" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input maxlength=100 {% if v.flairchanged %}disabled{% endif %} autocomplete="off" id="customtitlebody" type="text" name="title" class="form-control" placeholder='Enter a flair here' value="{% if v.customtitleplain %}{{v.customtitleplain}}{% endif %}">
<div class="d-flex mt-2">
<div role="button"><i class="btn btn-secondary format d-inline-block m-0 fas fa-smile-beam" onclick="loadEmojis('customtitlebody')" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji"></i></div>
<small class="ml-3">Limit of 100 characters</small>
<input {% if v.flairchanged %}disabled{% endif %} autocomplete="off" class="btn btn-primary ml-auto" id="titleSave" type="submit" onclick="disable(this)" value="Change Flair">
</div>
</form>
</div>
</div>
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Flair Color</label>
<div class="d-flex">
<form action="/settings/titlecolor" id="titlecolor-form" method="post" class="color-picker" style="line-height: 0">
<input type="hidden" name="formkey" value="{{v.formkey}}">
{% for titlecolor in COLORS %}
<input autocomplete="off" type="radio" name="titlecolor" id="titlecolor-{{titlecolor}}" value="{{titlecolor}}" {% if v.titlecolor == titlecolor %}checked{% endif %} onclick="document.getElementById('titlecolor-form').submit()">
<label class="color-radio" for="titlecolor-{{titlecolor}}">
<span style="background-color: #{{titlecolor}}">
{% if v.titlecolor.lower() == titlecolor %}
<i class="fas fa-check text-white"></i>
{% else %}
&nbsp;
{% endif %}
</span>
</label>
{% endfor %}
</form>
</div>
<p class="text-small mb-2">Or type a color hex code:</p>
<div class="d-flex">
<form action="/settings/titlecolor" id="color-code-form" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input autocomplete="off" class="form-control" type="text" name="titlecolor" id="color-code" minlength="6" maxlength="6" value="{% if v.titlecolor %}{{v.titlecolor}}{% endif %}">
<label class="btn btn-secondary text-capitalize mr-2 mt-2 mb-0">Update<input autocomplete="off" type="text" for="color-code" onclick="form.submit()" hidden=""></label>
</form>
</div>
</div>
{% if v.verified %}
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Checkmark Color</label>
<div class="d-flex">
<form action="/settings/verifiedcolor" id="verifiedcolor-form" method="post" class="color-picker" style="line-height: 0">
<input type="hidden" name="formkey" value="{{v.formkey}}">
{% for verifiedcolor in COLORS %}
<input autocomplete="off" type="radio" name="verifiedcolor" id="verifiedcolor-{{verifiedcolor}}" value="{{verifiedcolor}}" {% if v.verifiedcolor == verifiedcolor %}checked{% endif %} onclick="document.getElementById('verifiedcolor-form').submit()">
<label class="color-radio" for="verifiedcolor-{{verifiedcolor}}">
<span style="background-color: #{{verifiedcolor}}">
{% if v.verifiedcolor and v.verifiedcolor.lower() == verifiedcolor %}
<i class="fas fa-check text-white"></i>
{% else %}
&nbsp;
{% endif %}
</span>
</label>
{% endfor %}
</form>
</div>
<p class="text-small mb-2">Or type a color hex code:</p>
<div class="d-flex">
<form action="/settings/verifiedcolor" id="color-code-form" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input autocomplete="off" class="form-control" type="text" name="verifiedcolor" id="color-code" minlength="6" maxlength="6" value="{% if v.verifiedcolor %}{{v.verifiedcolor}}{% endif %}">
<label class="btn btn-secondary text-capitalize mr-2 mt-2 mb-0">Update<input autocomplete="off" type="text" for="color-code" onclick="form.submit()" hidden=""></label>
</form>
</div>
</div>
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Checkmark Hover Text</label>
<div class="w-lg-100">
<form action="/settings/checkmark_text" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input minlength=1 maxlength=100 autocomplete="off" id="checkmark_text" type="text" name="title" class="form-control" placeholder='Enter text here' value="{{v.verified}}">
<div class="d-flex mt-2">
<small>Limit of 100 characters</small>
<input autocomplete="off" class="btn btn-primary ml-auto" id="titleSave" type="submit" onclick="disable(this)" value="Change Text">
</div>
</form>
</div>
</div>
{% endif %}
{% if FEATURES['USERS_PROFILE_BODYTEXT'] -%}
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Bio</label>
<div class="w-lg-100">
<form id="profile-bio" action="/settings/profile" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea autocomplete="off" id="bio-text" class="form-control rounded" aria-label="With textarea" placeholder="Tell the community a bit about yourself." rows="3" name="bio" form="profile-bio" maxlength="1500">{% if v.bio %}{{v.bio}}{% endif %}</textarea>
<div class="d-flex">
<pre style="padding-top:0.7rem;line-height:1" class="btn btn-secondary format d-inline-block m-0 font-weight-bolder text-uppercase" onclick="commentForm('bio-text');getGif()" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#gifModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add GIF">GIF</pre>
&nbsp;
<pre style="padding-top:0.7rem" class="btn btn-secondary format d-inline-block m-0 fas fa-smile-beam" onclick="loadEmojis('bio-text')" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji"></pre>
&nbsp;
<label class="btn btn-secondary format d-inline-block m-0">
<div id="filename-show"><i class="fas fa-file"></i></div>
<input autocomplete="off" id="file-upload" accept="image/*, video/*, audio/*" type="file" name="file" multiple="multiple" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename-show','file-upload')" hidden>
</label>
</div>
<div class="d-flex mt-1">
<small>Limit of 1500 characters</small>
<input autocomplete="off" class="btn btn-primary ml-auto" id="bioSave" type="submit" onclick="disable(this)" value="Save Changes">
</div>
</form>
</div>
</div>
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Friends</label>
<div class="w-lg-100">
<form id="profile-friends" action="/settings/profile" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea autocomplete="off" id="friends-text" class="form-control rounded" aria-label="With textarea" placeholder="Enter your friends on the site..." rows="3" name="friends" form="profile-friends" maxlength="1500">{% if v.friends %}{{v.friends}}{% endif %}</textarea>
<div class="d-flex mt-1">
<small>Limit of 500 characters</small>
<input autocomplete="off" class="btn btn-primary ml-auto" id="friendsSave" type="submit" onclick="disable(this)" value="Save Changes">
</div>
</form>
</div>
</div>
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Enemies</label>
<div class="w-lg-100">
<form id="profile-enemies" action="/settings/profile" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea autocomplete="off" id="enemies-text" class="form-control rounded" aria-label="With textarea" placeholder="Enter your enemies on the site..." rows="3" name="enemies" form="profile-enemies" maxlength="1500">{% if v.enemies %}{{v.enemies}}{% endif %}</textarea>
<div class="d-flex mt-1">
<small>Limit of 500 characters</small>
<input autocomplete="off" class="btn btn-primary ml-auto" id="enemiesSave" type="submit" onclick="disable(this)" value="Save Changes">
</div>
</form>
</div>
</div>
{%- endif %}
{% if v.sig or v.patron %}
<div class="body d-lg-flex border-bottom">
<label class="text-black w-lg-25">Signature</label>
<div class="w-lg-100">
<form id="profile-sig" action="/settings/profile" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea autocomplete="off" id="sig-text" class="form-control rounded" aria-label="With textarea" placeholder="Enter a signature..." rows="3" name="sig" form="profile-sig" maxlength="200">{% if v.sig %}{{v.sig}}{% endif %}</textarea>
<div class="d-flex">
<pre style="padding-top:0.7rem;line-height:1" class="btn btn-secondary format d-inline-block m-0 font-weight-bolder text-uppercase" onclick="commentForm('sig-text');getGif()" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#gifModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add GIF">GIF</pre>
&nbsp;
<pre style="padding-top:0.7rem" class="btn btn-secondary format d-inline-block m-0 fas fa-smile-beam" onclick="loadEmojis('sig-text')" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji"></pre>
</div>
<div class="d-flex mt-1">
<small>Limit of 200 characters</small>
<input autocomplete="off" class="btn btn-primary ml-auto" id="sigSave" type="submit" onclick="disable(this)" value="Save Changes">
</div>
</form>
</div>
</div>
{% endif %}
<div class="d-lg-flex border-bottom">
<div class="title w-lg-25">
<label for="privateswitch">Private Mode</label>
</div>
<div class="body w-lg-100">
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="privateswitch" name="private"{% if v.is_private%} checked{% endif %} onchange="postToastSwitch(this,'/settings/profile?private='+document.getElementById('privateswitch').checked)">
<label class="custom-control-label" for="privateswitch"></label>
</div>
<span class="text-small text-muted">This will hide your post and comment history from others. We will also ask search engines to not index your profile page. (Your content will still be accessible via direct link.)</span>
</div>
</div>
<div class="d-lg-flex border-bottom">
<div class="title w-lg-25">
<label for="nofollowswitch">Disable Subscriptions</label>
</div>
<div class="body w-lg-100">
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="nofollowswitch" name="nofollow"{% if v.is_nofollow%} checked{% endif %} onchange="postToastSwitch(this,'/settings/profile?nofollow='+document.getElementById('nofollowswitch').checked)">
<label class="custom-control-label" for="nofollowswitch"></label>
</div>
<span class="text-small text-muted">Prevent other users from following you.</span>
</div>
</div>
<div class="d-lg-flex border-bottom">
<div class="title w-lg-25">
<label for="spiderswitch">Spider</label>
</div>
<div class="body w-lg-100">
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="spiderswitch" name="spider" {% if v.spider%}checked{% endif %} {% if v.spider and v.spider > 1 %}disabled{% endif %} onchange="postToastSwitch(this,'/settings/profile?spider='+document.getElementById('spiderswitch').checked)">
<label class="custom-control-label" for="spiderswitch"></label>
</div>
<span class="text-small text-muted">Have a spider friend accompany you during your journey on the site.</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include "emoji_modal.html" %}
{% include "gif_modal.html" %}
{% if v.flairchanged %}
<script>
const date = new Date({{v.flairchanged}}*1000).toString();
const text = ` - Your flair has been locked until ${date}`
document.getElementById('customtitlebody').value += text;
</script>
{% endif %}
<script defer src="{{'js/settings_profile.js' | asset}}"></script>
{% endblock %}