From ce48b0f3a0a24805cffabb188239f7c130b9f360 Mon Sep 17 00:00:00 2001 From: SneedBot Date: Sun, 1 Jan 2023 13:04:08 +0000 Subject: [PATCH] sneed --- files/routes/settings.py.save | 906 ++++++++++++++++++++++++++++++++++ 1 file changed, 906 insertions(+) create mode 100644 files/routes/settings.py.save diff --git a/files/routes/settings.py.save b/files/routes/settings.py.save new file mode 100644 index 0000000000..d3eb31deae --- /dev/null +++ b/files/routes/settings.py.save @@ -0,0 +1,906 @@ +from __future__ import unicode_literals + +import os +from shutil import copyfile + +import pyotp +import requests +import youtube_dl + +from files.helpers.actions import * +from files.helpers.alerts import * +from files.helpers.config.const import * +from files.helpers.get import * +from files.helpers.mail import * +from files.helpers.media import process_files, process_image +from files.helpers.regex import * +from files.helpers.sanitize import * +from files.helpers.sanitize import filter_emojis_only +from files.helpers.security import * +from files.helpers.useractions import * +from files.routes.wrappers import * + +from .front import frontlist +from files.__main__ import app, cache, limiter + + +@app.get("/settings") +@auth_required +def settings(v:User): + return redirect("/settings/personal") + +@app.get("/settings/personal") +@auth_required +def settings_personal(v:User): + return render_template("settings/personal.html", v=v) + +@app.delete('/settings/background') +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def remove_background(v): + if v.background: + if v.background.startswith('/images/'): + os.remove(v.background) + v.background = None + g.db.add(v) + return {"message": "Background removed!"} + +@app.post('/settings/custom_background') +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def upload_custom_background(v): + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") + + if not v.patron: + abort(403, f"This feature is only available to {patron}s!") + + file = request.files["file"] + + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + background = process_image(name, v) + + if background: + if v.background and v.background.startswith('/images/'): + os.remove(v.background) + v.background = background + g.db.add(v) + + return redirect('/settings/personal') + +@app.post('/settings/profile_background') +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def upload_profile_background(v): + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") + + file = request.files["file"] + + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + background = process_image(name, v) + + if background: + if v.profile_background and path.isfile(v.profile_background): + os.remove(v.profile_background) + v.profile_background = background + g.db.add(v) + badge_grant(badge_id=193, user=v) + return redirect(f'/@{v.username}') + +@app.delete('/settings/profile_background') +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def delete_profile_background(v): + if v.profile_background: + os.remove(v.profile_background) + v.profile_background = None + return {"message": "Profile background removed!"} + +@app.post("/settings/personal") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_personal_post(v): + if v.id == 253 and request.values.get("private"): + abort(403) + updated = False + + # begin common selectors # + + def update_flag(column_name:str, request_name:str): + if not request.values.get(request_name, ''): return False + request_flag = request.values.get(request_name, '') == 'true' + if request_flag != getattr(v, column_name): + setattr(v, column_name, request_flag) + return True + return False + + def update_potentially_permanent_flag(column_name:str, request_name:str, friendly_name:str, badge_id:Optional[int]): + if not request.values.get(request_name): return False + current_value = getattr(v, column_name) + if FEATURES['USERS_PERMANENT_WORD_FILTERS'] and current_value > 1: + abort(403, f"Cannot change the {friendly_name} setting after you've already set it permanently!") + request_flag = int(request.values.get(request_name, '') == 'true') + if current_value and request_flag and request.values.get("permanent", '') == 'true' and request.values.get("username") == v.username: + if v.client: abort(403, f"Cannot set {friendly_name} permanently from the API") + request_flag = int(time.time()) + setattr(v, column_name, request_flag) + if badge_id: badge_grant(v, badge_id) + return render_template("settings/personal.html", v=v, msg=f"You have set the {friendly_name} permanently! Enjoy your new badge!") + elif current_value != request_flag: + setattr(v, column_name, request_flag) + return True + return False + + def set_selector_option(column_name:str, api_name:str, valid_values:Iterable[str], error_msg:str="value"): + opt = request.values.get(api_name) + if opt: opt = opt.strip() + if not opt: return False + if opt in valid_values: + setattr(v, column_name, opt) + return True + abort(400, f"'{opt}' is not a valid {error_msg}") + + # end common selectors # + + background = request.values.get("background", v.background) + if background != v.background and background.endswith(".webp") and len(background) <= 20: + v.background = '/i/backgrounds/' + request.values.get("background") + updated = True + elif request.values.get("reddit", v.reddit) != v.reddit: + reddit = request.values.get("reddit") + if reddit in {'old.reddit.com', 'reddit.com', 'i.reddit.com', 'teddit.net', 'libredd.it', 'unddit.com'}: + updated = True + v.reddit = reddit + elif request.values.get("poor", v.poor) != v.poor: + updated = True + v.poor = request.values.get("poor", v.poor) == 'true' + session['poor'] = v.poor + + slur_filter_updated = updated or update_potentially_permanent_flag("slurreplacer", "slurreplacer", "slur replacer", 192) + if isinstance(slur_filter_updated, bool): + updated = slur_filter_updated + else: + g.db.add(v) + return slur_filter_updated + + profanity_filter_updated = updated or update_potentially_permanent_flag("profanityreplacer", "profanityreplacer", "profanity replacer", 190) + if isinstance(profanity_filter_updated, bool): + updated = profanity_filter_updated + else: + g.db.add(v) + return profanity_filter_updated + + updated = updated or update_flag("hidevotedon", "hidevotedon") + updated = updated or update_flag("cardview", "cardview") + updated = updated or update_flag("newtab", "newtab") + updated = updated or update_flag("newtabexternal", "newtabexternal") + updated = updated or update_flag("nitter", "nitter") + updated = updated or update_flag("imginn", "imginn") + updated = updated or update_flag("controversial", "controversial") + updated = updated or update_flag("sigs_disabled", "sigs_disabled") + updated = updated or update_flag("over_18", "over_18") + updated = updated or update_flag("is_private", "private") + + if not updated and request.values.get("spider", v.spider) != v.spider and v.spider <= 1: + updated = True + v.spider = int(request.values.get("spider") == 'true') + if v.spider: badge_grant(user=v, badge_id=179) + else: + badge = v.has_badge(179) + if badge: g.db.delete(badge) +<< + elif IS_FISTMAS() and not updated and request.values.get("event_music", v.event_music) != v.event_music and v.can_toggle_event_music: + updated = True + v.event_music = not v.event_music + + elif not updated and request.values.get("bio") == "" and not request.files.get('file'): + v.bio = None + v.bio_html = None + g.db.add(v) + return render_template("settings/personal.html", v=v, msg="Your bio has been updated.") + + elif not updated and request.values.get("sig") == "": + v.sig = None + v.sig_html = None + g.db.add(v) + return render_template("settings/personal.html", v=v, msg="Your sig has been updated.") + + elif not updated and request.values.get("friends") == "": + v.friends = None + v.friends_html = None + g.db.add(v) + return render_template("settings/personal.html", v=v, msg="Your friends list has been updated.") + + elif not updated and request.values.get("enemies") == "": + v.enemies = None + v.enemies_html = None + g.db.add(v) + return render_template("settings/personal.html", v=v, msg="Your enemies list has been updated.") + + elif not updated and v.patron and request.values.get("sig"): + sig = request.values.get("sig")[:200].replace('\n','').replace('\r','') + sig_html = sanitize(sig) + if len(sig_html) > 1000: + 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/personal.html", + v=v, + msg="Your sig has been updated.") + + elif not updated and FEATURES['USERS_PROFILE_BODYTEXT'] and request.values.get("friends"): + friends = request.values.get("friends")[:1000] + + friends_html = sanitize(friends) + + if len(friends_html) > 5000: + return render_template("settings/personal.html", + v=v, + error="Your friends list is too long") + + notify_users = NOTIFY_USERS(friends, v) + + if notify_users: + cid = notif_comment(f"@{v.username} has added you to their friends list!") + for x in notify_users: + add_notif(cid, x) + + v.friends = friends[:1000] + v.friends_html=friends_html + g.db.add(v) + return render_template("settings/personal.html", + v=v, + msg="Your friends list has been updated.") + + + elif not updated and FEATURES['USERS_PROFILE_BODYTEXT'] and request.values.get("enemies"): + enemies = request.values.get("enemies")[:1000] + + enemies_html = sanitize(enemies) + + if len(enemies_html) > 5000: + return render_template("settings/personal.html", + v=v, + error="Your enemies list is too long") + + notify_users = NOTIFY_USERS(enemies, v) + if notify_users: + cid = notif_comment(f"@{v.username} has added you to their enemies list!") + for x in notify_users: + add_notif(cid, x) + + v.enemies = enemies[:1000] + v.enemies_html=enemies_html + g.db.add(v) + return render_template("settings/personal.html", + v=v, + msg="Your enemies list has been updated.") + + + elif not updated and FEATURES['USERS_PROFILE_BODYTEXT'] and \ + (request.values.get("bio") or request.files.get('file')): + bio = request.values.get("bio")[:1500] + bio += process_files(request.files, v) + bio = bio.strip() + bio_html = sanitize(bio) + + if len(bio_html) > 10000: + return render_template("settings/personal.html", + v=v, + error="Your bio is too long") + + if len(bio_html) > 10000: abort(400) + + v.bio = bio[:1500] + v.bio_html=bio_html + g.db.add(v) + return render_template("settings/personal.html", + v=v, + msg="Your bio has been updated.") + + + frontsize = request.values.get("frontsize") + if frontsize: + frontsize = int(frontsize) + if frontsize in PAGE_SIZES: + v.frontsize = frontsize + updated = True + cache.delete_memoized(frontlist) + else: abort(400) + + updated = updated or set_selector_option("defaultsortingcomments", "defaultsortingcomments", COMMENT_SORTS, "comment sort") + updated = updated or set_selector_option("defaultsorting", "defaultsorting", SORTS, "post sort") + updated = updated or set_selector_option("defaulttime", "defaulttime", TIME_FILTERS, "time filter") + + theme = request.values.get("theme") + if not updated and theme: + if theme in THEMES: + if v.theme == "win98": v.themecolor = DEFAULT_COLOR + v.theme = theme + if theme == "win98": v.themecolor = "30409f" + updated = True + else: abort(400, f"{theme} is not a valid theme") + + house = request.values.get("house") + if not updated and house and house in HOUSES and FEATURES['HOUSES']: + if v.bite: abort(403) + if v.house: + if v.house.replace(' Founder', '') == house: abort(409, f"You're already in House {house}") + cost = HOUSE_SWITCH_COST + else: + cost = HOUSE_JOIN_COST + + success = v.charge_account('coins', cost) + if not success: + success = v.charge_account('marseybux', cost) + if not success: abort(403) + + if house == "None": house = '' + v.house = house + + updated = True + + if updated: + g.db.add(v) + return {"message": "Your settings have been updated."} + else: + abort(400, "You didn't change anything.") + + +@app.post("/settings/filters") +@auth_required +def filters(v:User): + filters=request.values.get("filters")[:1000].strip() + + if filters == v.custom_filter_list: + return render_template("settings/advanced.html", v=v, error="You didn't change anything") + + v.custom_filter_list=filters + g.db.add(v) + return render_template("settings/advanced.html", v=v, msg="Your custom filters have been updated.") + + +def set_color(v:User, attr:str, color:Optional[str]): + current = getattr(v, attr) + color = color.strip().lower() if color else None + if color: + if color.startswith('#'): color = color[1:] + if not color_regex.fullmatch(color): + return render_template("settings/personal.html", v=v, error="Invalid color hex code!") + if color and current != color: + setattr(v, attr, color) + g.db.add(v) + return render_template("settings/personal.html", v=v, msg="Color successfully updated!") + + +@app.post("/settings/namecolor") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def namecolor(v): + return set_color(v, "namecolor", request.values.get("namecolor")) + +@app.post("/settings/themecolor") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def themecolor(v): + return set_color(v, "themecolor", request.values.get("themecolor")) + +@app.post("/settings/gumroad") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def gumroad(v): + if GUMROAD_TOKEN == DEFAULT_CONFIG_VALUE: abort(404) + if not (v.email and v.is_activated): + abort(400, f"You must have a verified email to verify {patron} status and claim your rewards!") + + data = {'access_token': GUMROAD_TOKEN, 'email': v.email} + response = requests.get('https://api.gumroad.com/v2/sales', data=data, timeout=5).json()["sales"] + + if len(response) == 0: abort(404, "Email not found") + + response = [x for x in response if x['variants_and_quantity']] + response = response[0] + tier = tiers[response["variants_and_quantity"]] + if v.patron == tier: abort(400, f"{patron} rewards already claimed") + + marseybux = marseybux_li[tier] - marseybux_li[v.patron] + if marseybux < 0: abort(400, f"{patron} rewards already claimed") + + existing = g.db.query(User.id).filter(User.email == v.email, User.is_activated == True, User.patron >= tier).first() + if existing: abort(400, f"{patron} rewards already claimed on another account") + + v.patron = tier + + v.pay_account('marseybux', marseybux) + send_repeatable_notification(v.id, f"You have received {marseybux} Marseybux! You can use them to buy awards in the [shop](/shop).") + + g.db.add(v) + + badge_grant(badge_id=20+tier, user=v) + + + return {"message": f"{patron} rewards claimed!"} + +@app.post("/settings/titlecolor") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def titlecolor(v): + return set_color(v, "titlecolor", request.values.get("titlecolor")) + +@app.post("/settings/verifiedcolor") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def verifiedcolor(v): + if not v.verified: abort(403, "You don't have a checkmark") + return set_color(v, "verifiedcolor", request.values.get("verifiedcolor")) + +@app.post("/settings/security") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_security_post(v): + if request.values.get("new_password"): + if request.values.get("new_password") != request.values.get("cnf_password"): + return render_template("settings/security.html", v=v, error="Passwords do not match.") + + if not valid_password_regex.fullmatch(request.values.get("new_password")): + return render_template("settings/security.html", v=v, error="Password must be between 8 and 100 characters.") + + if not v.verifyPass(request.values.get("old_password")): + return render_template("settings/security.html", v=v, error="Incorrect password") + + v.passhash = hash_password(request.values.get("new_password")) + + g.db.add(v) + return render_template("settings/security.html", v=v, msg="Your password has been changed.") + + if request.values.get("new_email"): + if not v.verifyPass(request.values.get('password')): + return render_template("settings/security.html", v=v, error="Invalid password.") + + new_email = request.values.get("new_email","").strip().lower() + + if new_email == v.email: + return render_template("settings/security.html", v=v, error="This email is already yours!") + + url = f"{SITE_FULL}/activate" + + now = int(time.time()) + + token = generate_hash(f"{new_email}+{v.id}+{now}") + params = f"?email={quote(new_email)}&id={v.id}&time={now}&token={token}" + + link = url + params + + send_mail(to_address=new_email, + subject="Verify your email address.", + html=render_template("email/email_change.html", + action_url=link, + v=v) + ) + + return render_template("settings/security.html", v=v, msg="Check your email and click the verification link to complete the email change.") + + if request.values.get("2fa_token"): + if not v.verifyPass(request.values.get('password')): + return render_template("settings/security.html", v=v, error="Invalid password.") + + secret = request.values.get("2fa_secret") + x = pyotp.TOTP(secret) + if not x.verify(request.values.get("2fa_token"), valid_window=1): + return render_template("settings/security.html", v=v, error="Invalid token.") + + v.mfa_secret = secret + g.db.add(v) + return render_template("settings/security.html", v=v, msg="Two-factor authentication enabled.") + + if request.values.get("2fa_remove"): + if not v.verifyPass(request.values.get('password')): + return render_template("settings/security.html", v=v, error="Invalid password.") + + token = request.values.get("2fa_remove") + + if not token or not v.validate_2fa(token): + return render_template("settings/security.html", v=v, error="Invalid token.") + + v.mfa_secret = None + g.db.add(v) + g.db.commit() + return render_template("settings/security.html", v=v, msg="Two-factor authentication disabled.") + +@app.post("/settings/log_out_all_others") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_log_out_others(v): + submitted_password = request.values.get("password", "").strip() + if not v.verifyPass(submitted_password): + return render_template("settings/security.html", v=v, error="Incorrect Password"), 401 + + v.login_nonce += 1 + session["login_nonce"] = v.login_nonce + g.db.add(v) + return render_template("settings/security.html", v=v, msg="All other devices have been logged out") + + +@app.post("/settings/images/profile") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_images_profile(v): + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") + + file = request.files["profile"] + + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + highres = process_image(name, v) + + if not highres: abort(400) + + name2 = name.replace('.webp', 'r.webp') + copyfile(name, name2) + imageurl = process_image(name2, v, resize=100) + + if not imageurl: abort(400) + + if v.highres and '/images/' in v.highres: + os.remove(v.highres) + + if v.profileurl and '/images/' in v.profileurl: + os.remove(v.profileurl) + v.highres = highres + v.profileurl = imageurl + g.db.add(v) + + + return render_template("settings/personal.html", v=v, msg="Profile picture successfully updated.") + + +@app.post("/settings/images/banner") +@feature_required('USERS_PROFILE_BANNER') +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_images_banner(v): + if g.is_tor: abort(403, "Image uploads are not allowed through TOR.") + + file = request.files["banner"] + + name = f'/images/{time.time()}'.replace('.','') + '.webp' + file.save(name) + bannerurl = process_image(name, v) + + if bannerurl: + if v.bannerurl and '/images/' in v.bannerurl and path.isfile(v.bannerurl): + os.remove(v.bannerurl) + v.bannerurl = bannerurl + g.db.add(v) + + return render_template("settings/personal.html", v=v, msg="Banner successfully updated.") + +@app.get("/settings/css") +@auth_required +def settings_css_get(v:User): + return render_template("settings/css.html", v=v) + +@app.post("/settings/css") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_css(v): + if v.agendaposter: abort(400, "Agendapostered users can't edit CSS!") + css = request.values.get("css", v.css).strip().replace('\\', '').strip()[:CSS_LENGTH_LIMIT] + v.css = css + g.db.add(v) + + return render_template("settings/css.html", v=v, msg="Custom CSS successfully updated!") + +@app.post("/settings/profilecss") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_profilecss(v): + profilecss = request.values.get("profilecss", v.profilecss).strip().replace('\\', '').strip()[:CSS_LENGTH_LIMIT] + valid, error = validate_css(profilecss) + if not valid: + return render_template("settings/css.html", error=error, v=v) + v.profilecss = profilecss + g.db.add(v) + return render_template("settings/css.html", v=v, msg="Profile CSS successfully updated!") + +@app.get("/settings/security") +@auth_required +def settings_security(v:User): + 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") +@limiter.limit("1/second;20/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@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: + if not v.shadowbanned: + send_notification(user.id, f"@{v.username} has tried to block you and failed because of your unblockable status!") + abort(403, f"@{user.username} is unblockable!") + + if user.id == v.id: abort(400, "You can't block yourself") + if user.id == AUTOJANNY_ID: abort(403, f"You can't block @{user.username}") + 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) + 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."} + + +@app.post("/settings/unblock") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_unblock_user(v): + user = get_user(request.values.get("username")) + 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 successfully!"} + +@app.get("/settings/apps") +@auth_required +def settings_apps(v:User): + return render_template("settings/apps.html", v=v) + +@app.get("/settings/advanced") +@auth_required +def settings_advanced_get(v:User): + return render_template("settings/advanced.html", v=v) + +@app.post("/settings/name_change") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@is_not_permabanned +def settings_name_change(v): + new_name=request.values.get("name").strip() + + if new_name==v.username: + 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/personal.html", + v=v, + error="This isn't a valid username.") + + search_name = new_name.replace('\\', '').replace('_','\_').replace('%','') + + x = g.db.query(User).filter( + or_( + User.username.ilike(search_name), + User.original_username.ilike(search_name) + ) + ).one_or_none() + + if x and x.id != v.id: + return render_template("settings/personal.html", + v=v, + error=f"Username `{new_name}` is already in use.") + + v=get_account(v.id) + v.username=new_name + v.name_changed_utc=int(time.time()) + g.db.add(v) + + return render_template("settings/personal.html", v=v, msg="Name successfully changed!") + +@app.post("/settings/song_change_mp3") +@feature_required('USERS_PROFILE_SONG') +@limiter.limit("1/second;10/day") +@limiter.limit("1/second;10/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_song_change_mp3(v): + file = request.files['file'] + if file.content_type != 'audio/mpeg': + return render_template("settings/personal.html", v=v, error="Not a valid MP3 file") + + song = str(time.time()).replace('.','') + + name = f'/songs/{song}.mp3' + file.save(name) + + size = os.stat(name).st_size + if size > 8 * 1024 * 1024: + os.remove(name) + 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") + + v.song = song + g.db.add(v) + + return render_template("settings/personal.html", v=v, msg="Profile Anthem successfully updated!") + +@app.post("/settings/song_change") +@feature_required('USERS_PROFILE_SONG') +@limiter.limit("3/second;10/day") +@limiter.limit("3/second;10/day", key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_song_change(v): + song=request.values.get("song").strip() + + if song == "" and v.song: + 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") + v.song = None + g.db.add(v) + return render_template("settings/personal.html", v=v, msg="Profile Anthem successfully removed!") + + song = song.replace("https://music.youtube.com", "https://youtube.com") + if song.startswith(("https://www.youtube.com/watch?v=", "https://youtube.com/watch?v=", "https://m.youtube.com/watch?v=")): + id = song.split("v=")[1] + elif song.startswith("https://youtu.be/"): + id = song.split("https://youtu.be/")[1] + else: + return render_template("settings/personal.html", v=v, error="Not a YouTube link"), 400 + + if "?" in id: id = id.split("?")[0] + if "&" in id: id = id.split("&")[0] + + if not yt_id_regex.fullmatch(id): + return render_template("settings/personal.html", v=v, error="Not a YouTube link"), 400 + if path.isfile(f'/songs/{id}.mp3'): + v.song = id + g.db.add(v) + return render_template("settings/personal.html", v=v, msg="Profile Anthem successfully updated!") + + + 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/personal.html", v=v, error="Can't use a live youtube video!"), 400 + + if "H" in duration: + return render_template("settings/personal.html", v=v, error="Duration of the video must not exceed 15 minutes."), 400 + + if "M" in duration: + duration = int(duration.split("PT")[1].split("M")[0]) + if duration > 15: + return render_template("settings/personal.html", v=v, error="Duration of the video must not exceed 15 minutes."), 400 + + + if v.song and 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") + + ydl_opts = { + 'cookiefile': '.cookies', + 'outtmpl': '/songs/%(title)s.%(ext)s', + 'format': 'bestaudio/best', + 'postprocessors': [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192', + }], + } + + with youtube_dl.YoutubeDL(ydl_opts) as ydl: + try: ydl.download([f"https://youtube.com/watch?v={id}"]) + except Exception as e: + print(e, flush=True) + return render_template("settings/personal.html", + v=v, + error="Age-restricted videos aren't allowed."), 400 + + files = os.listdir("/songs/") + paths = [path.join("/songs/", basename) for basename in files] + songfile = max(paths, key=path.getctime) + os.rename(songfile, f"/songs/{id}.mp3") + + v.song = id + g.db.add(v) + return render_template("settings/personal.html", v=v, msg="Profile Anthem successfully updated!") + +@app.post("/settings/title_change") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_title_change(v): + if v.flairchanged: abort(403) + + customtitleplain = sanitize_settings_text(request.values.get("title"), 100) + + if len(customtitleplain) > 100: + return render_template("settings/personal.html", v=v, error="Flair too long!") + + if customtitleplain == v.customtitleplain: + 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/personal.html", v=v, error="Flair too long!") + + v.customtitleplain = customtitleplain + v.customtitle = customtitle + g.db.add(v) + + return render_template("settings/personal.html", v=v, msg="Flair successfully updated!") + + +@app.post("/settings/pronouns_change") +@feature_required('PRONOUNS') +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_pronouns_change(v): + pronouns = sanitize_settings_text(request.values.get("pronouns")) + + if len(pronouns) > 11: + 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/personal.html", v=v, error="You didn't change anything.") + + if not pronouns_regex.fullmatch(pronouns): + 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' + elif 'fag' in bare_pronouns: pronouns = 'cute/twink' + + v.pronouns = pronouns + g.db.add(v) + + return render_template("settings/personal.html", v=v, msg="Pronouns successfully updated!") + + +@app.post("/settings/checkmark_text") +@limiter.limit(DEFAULT_RATELIMIT_SLOWER) +@limiter.limit(DEFAULT_RATELIMIT_SLOWER, key_func=lambda:f'{request.host}-{session.get("lo_user")}') +@auth_required +def settings_checkmark_text(v): + if not v.verified: abort(403) + new_name = sanitize_settings_text(request.values.get("checkmark-text"), 100) + if not new_name: abort(400) + 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 render_template("settings/personal.html", v=v, msg="Checkmark Text successfully updated!") + +if IS_FISTMAS(): + @app.post("/events/fistmas2022/darkmode") + @auth_required + def event_darkmode(v): + v.event_darkmode = not v.event_darkmode + g.db.add(v) + return {"message": "Dark mode toggled successfully!"}