Convert text file line endings to LF.

This commit is contained in:
Ben Rog-Wilhelm 2022-05-09 07:20:47 -05:00 committed by GitHub
parent 67d5db425f
commit 1ee9c1bfa7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
166 changed files with 33162 additions and 33162 deletions

8
.gitattributes vendored
View file

@ -1,4 +1,4 @@
*.css linguist-detectable=false
*.js linguist-detectable=true
*.html linguist-detectable=false
*.py linguist-detectable=true
*.css linguist-detectable=false
*.js linguist-detectable=true
*.html linguist-detectable=false
*.py linguist-detectable=true

View file

@ -1,17 +1,17 @@
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt -y upgrade && apt install -y supervisor python3-pip libenchant1c2a ffmpeg
COPY supervisord.conf /etc/supervisord.conf
COPY requirements.txt /etc/requirements.txt
RUN pip3 install -r /etc/requirements.txt
RUN mkdir /images && mkdir /songs
EXPOSE 80/tcp
CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt -y upgrade && apt install -y supervisor python3-pip libenchant1c2a ffmpeg
COPY supervisord.conf /etc/supervisord.conf
COPY requirements.txt /etc/requirements.txt
RUN pip3 install -r /etc/requirements.txt
RUN mkdir /images && mkdir /songs
EXPOSE 80/tcp
CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ]

1322
LICENSE

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"

View file

@ -1,124 +1,124 @@
import gevent.monkey
gevent.monkey.patch_all()
from os import environ, path
import secrets
from flask import *
from flask_caching import Cache
from flask_limiter import Limiter
from flask_compress import Compress
from flask_mail import Mail
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import *
import gevent
import redis
import time
from sys import stdout, argv
import faulthandler
import json
app = Flask(__name__, template_folder='templates')
app.url_map.strict_slashes = False
app.jinja_env.cache = {}
app.jinja_env.auto_reload = True
faulthandler.enable()
app.config["SITE_NAME"]=environ.get("SITE_NAME").strip()
app.config["GUMROAD_LINK"]=environ.get("GUMROAD_LINK", "https://marsey1.gumroad.com/l/tfcvri").strip()
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['DATABASE_URL'] = environ.get("DATABASE_URL", "postgresql://postgres@localhost:5432")
app.config['SECRET_KEY'] = environ.get('MASTER_KEY')
app.config["SERVER_NAME"] = environ.get("DOMAIN").strip()
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3153600
app.config["SESSION_COOKIE_NAME"] = "session_" + environ.get("SITE_NAME").strip().lower()
app.config["VERSION"] = "1.0.0"
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 365
app.config["DEFAULT_COLOR"] = environ.get("DEFAULT_COLOR", "ff0000").strip()
app.config["DEFAULT_THEME"] = environ.get("DEFAULT_THEME", "midnight").strip()
app.config["FORCE_HTTPS"] = 1
app.config["UserAgent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
app.config["HCAPTCHA_SITEKEY"] = environ.get("HCAPTCHA_SITEKEY","").strip()
app.config["HCAPTCHA_SECRET"] = environ.get("HCAPTCHA_SECRET","").strip()
app.config["SPAM_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_SIMILARITY_THRESHOLD", 0.5))
app.config["SPAM_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", 0.1))
app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", 10))
app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"] = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD", 0.5))
app.config["COMMENT_SPAM_COUNT_THRESHOLD"] = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD", 10))
app.config["CACHE_TYPE"] = "RedisCache"
app.config["CACHE_REDIS_URL"] = environ.get("REDIS_URL", "redis://localhost")
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = environ.get("MAIL_USERNAME", "").strip()
app.config['MAIL_PASSWORD'] = environ.get("MAIL_PASSWORD", "").strip()
app.config['DESCRIPTION'] = environ.get("DESCRIPTION", "DESCRIPTION GOES HERE").strip()
app.config['SETTINGS'] = {}
r=redis.Redis(host=environ.get("REDIS_URL", "redis://localhost"), decode_responses=True, ssl_cert_reqs=None)
def get_CF():
with app.app_context():
return request.headers.get('CF-Connecting-IP')
limiter = Limiter(
app,
key_func=get_CF,
default_limits=["3/second;30/minute;200/hour;1000/day"],
application_limits=["10/second;200/minute;5000/hour;10000/day"],
storage_uri=environ.get("REDIS_URL", "redis://localhost")
)
Base = declarative_base()
engine = create_engine(app.config['DATABASE_URL'])
db_session = scoped_session(sessionmaker(bind=engine, autoflush=False))
cache = Cache(app)
Compress(app)
mail = Mail(app)
@app.before_request
def before_request():
with open('site_settings.json', 'r') as f:
app.config['SETTINGS'] = json.load(f)
if request.host != app.config["SERVER_NAME"]: return {"error":"Unauthorized host provided."}, 401
if request.headers.get("CF-Worker"): return {"error":"Cloudflare workers are not allowed to access this website."}, 401
if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"): abort(503)
g.db = db_session()
ua = request.headers.get("User-Agent","").lower()
if '; wv) ' in ua: g.webview = True
else: g.webview = False
if 'iphone' in ua or 'ipad' in ua or 'ipod' in ua or 'mac os' in ua or ' firefox/' in ua: g.inferior_browser = True
else: g.inferior_browser = False
g.timestamp = int(time.time())
@app.teardown_appcontext
def teardown_request(error):
if hasattr(g, 'db') and g.db:
g.db.close()
stdout.flush()
@app.after_request
def after_request(response):
response.headers.add("Strict-Transport-Security", "max-age=31536000")
response.headers.add("X-Frame-Options", "deny")
return response
if "load_chat" in argv:
from files.routes.chat import *
else:
from files.routes import *
import gevent.monkey
gevent.monkey.patch_all()
from os import environ, path
import secrets
from flask import *
from flask_caching import Cache
from flask_limiter import Limiter
from flask_compress import Compress
from flask_mail import Mail
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import *
import gevent
import redis
import time
from sys import stdout, argv
import faulthandler
import json
app = Flask(__name__, template_folder='templates')
app.url_map.strict_slashes = False
app.jinja_env.cache = {}
app.jinja_env.auto_reload = True
faulthandler.enable()
app.config["SITE_NAME"]=environ.get("SITE_NAME").strip()
app.config["GUMROAD_LINK"]=environ.get("GUMROAD_LINK", "https://marsey1.gumroad.com/l/tfcvri").strip()
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['DATABASE_URL'] = environ.get("DATABASE_URL", "postgresql://postgres@localhost:5432")
app.config['SECRET_KEY'] = environ.get('MASTER_KEY')
app.config["SERVER_NAME"] = environ.get("DOMAIN").strip()
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3153600
app.config["SESSION_COOKIE_NAME"] = "session_" + environ.get("SITE_NAME").strip().lower()
app.config["VERSION"] = "1.0.0"
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 365
app.config["DEFAULT_COLOR"] = environ.get("DEFAULT_COLOR", "ff0000").strip()
app.config["DEFAULT_THEME"] = environ.get("DEFAULT_THEME", "midnight").strip()
app.config["FORCE_HTTPS"] = 1
app.config["UserAgent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
app.config["HCAPTCHA_SITEKEY"] = environ.get("HCAPTCHA_SITEKEY","").strip()
app.config["HCAPTCHA_SECRET"] = environ.get("HCAPTCHA_SECRET","").strip()
app.config["SPAM_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_SIMILARITY_THRESHOLD", 0.5))
app.config["SPAM_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", 0.1))
app.config["SPAM_SIMILAR_COUNT_THRESHOLD"] = int(environ.get("SPAM_SIMILAR_COUNT_THRESHOLD", 10))
app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"] = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD", 0.5))
app.config["COMMENT_SPAM_COUNT_THRESHOLD"] = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD", 10))
app.config["CACHE_TYPE"] = "RedisCache"
app.config["CACHE_REDIS_URL"] = environ.get("REDIS_URL", "redis://localhost")
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = environ.get("MAIL_USERNAME", "").strip()
app.config['MAIL_PASSWORD'] = environ.get("MAIL_PASSWORD", "").strip()
app.config['DESCRIPTION'] = environ.get("DESCRIPTION", "DESCRIPTION GOES HERE").strip()
app.config['SETTINGS'] = {}
r=redis.Redis(host=environ.get("REDIS_URL", "redis://localhost"), decode_responses=True, ssl_cert_reqs=None)
def get_CF():
with app.app_context():
return request.headers.get('CF-Connecting-IP')
limiter = Limiter(
app,
key_func=get_CF,
default_limits=["3/second;30/minute;200/hour;1000/day"],
application_limits=["10/second;200/minute;5000/hour;10000/day"],
storage_uri=environ.get("REDIS_URL", "redis://localhost")
)
Base = declarative_base()
engine = create_engine(app.config['DATABASE_URL'])
db_session = scoped_session(sessionmaker(bind=engine, autoflush=False))
cache = Cache(app)
Compress(app)
mail = Mail(app)
@app.before_request
def before_request():
with open('site_settings.json', 'r') as f:
app.config['SETTINGS'] = json.load(f)
if request.host != app.config["SERVER_NAME"]: return {"error":"Unauthorized host provided."}, 401
if request.headers.get("CF-Worker"): return {"error":"Cloudflare workers are not allowed to access this website."}, 401
if not app.config['SETTINGS']['Bots'] and request.headers.get("Authorization"): abort(503)
g.db = db_session()
ua = request.headers.get("User-Agent","").lower()
if '; wv) ' in ua: g.webview = True
else: g.webview = False
if 'iphone' in ua or 'ipad' in ua or 'ipod' in ua or 'mac os' in ua or ' firefox/' in ua: g.inferior_browser = True
else: g.inferior_browser = False
g.timestamp = int(time.time())
@app.teardown_appcontext
def teardown_request(error):
if hasattr(g, 'db') and g.db:
g.db.close()
stdout.flush()
@app.after_request
def after_request(response):
response.headers.add("Strict-Transport-Security", "max-age=31536000")
response.headers.add("X-Frame-Options", "deny")
return response
if "load_chat" in argv:
from files.routes.chat import *
else:
from files.routes import *

View file

@ -1,151 +1,151 @@
@charset "UTF-8";
:root {
--dark: #3E2C18;
--secondary: #FFE;
--white: #F1E7D0;
--black: #433434;
--light: #FFE;
--muted: #AA9881;
--gray: #AA9881;
--gray-100: #817261;
--gray-200: #433434;
--gray-300: #433434;
--gray-400: #AA9881;
--gray-500: #F0E0D6;
--gray-600: #FFE;
--gray-700: #F0E0D6;
--gray-800: #F0E0D6;
--gray-900: #F0E0D6;
--background: #F0E0D6;
}
.text-muted {
color: var(--black) !important;
}
.text-white {
color: var(--primary) !important;
}
.modal .comment-actions a {
color: var(--gray-500);
}
.navbar-light .navbar-nav .nav-link .fa, .navbar-light .navbar-nav .nav-link .fas, .navbar-light .navbar-nav .nav-link .far, .navbar-light .navbar-nav .nav-link .fab {
color: var(--black);
}
.dropdown-item:hover, .dropdown-item:focus, .dropdown-item.active {
background-color: #bda78e !important;
}
html * {
font-family: arial, helvetica, sans-serif;
color: #800000
}
body, #main-content-col {
background-color: var(--secondary) !important;
}
.border-right, .border-left, .border-top, .border-bottom, .border {
border-color: #D9BFB7 !important
}
.card-header {
border-color: #D9BFB7 !important
}
.post-title a {
color: #cc1105 !important;
}
.comment, .card {
background-color: var(--secondary) !important;
border-color: #D9BFB7 !important;
}
.form-control {
border-color: #D9BFB7 !important;
}
.post-actions *:hover, .comment-actions *:hover, .comment-actions .copy-link:hover {
color: #F00 !important
}
a {
color: #00e;
}
blockquote p {
color: #789922 !important;
}
.transparent {
background: None !important;
}
.btn {
color: var(--primary) !important;
}
#profilestuff > * {
color: #cfcfcf !important;
}
.comment {
background-color: var(--background) !important;
}
.navbar-light, .navbar .container-fluid, #mobile-bottom-navigation-bar {
background:#ffe url(https://i.imgur.com/kAfDGxF.png);
background-size: contain;
}
blockquote {
background-color: transparent;
border-color: #789922 !important;
border-width: 1px !important;
}
blockquote a {
color: navy;
}
.comment-collapse-desktop {
border-color: maroon !important;
border-width: 1px !important;
}
.score-up, .active.arrow-up::before, .arrow-up::after, .arrow-up:hover::before {
color: maroon !important;
}
.score-down, .active.arrow-down::before, .arrow-down::after, .arrow-down:hover::before {
color: maroon !important;
}
.arrow-up::before, .arrow-down::before, .score {
color: var(--gray-400);
}
.srd {
background-color: var(--background) !important;
}
.srd a {
color: maroon !important;
}
:root {
--primary: maroon;
}
.caction {
color: var(--gray-400) !important;
}
.user-name span {
color: #117743 !important;
}
.comment-anchor:target, .unread {
background: #ffffff88 !important;
}
#frontpage .post-title a:visited, .visited {
color: #949494 !important;
@charset "UTF-8";
:root {
--dark: #3E2C18;
--secondary: #FFE;
--white: #F1E7D0;
--black: #433434;
--light: #FFE;
--muted: #AA9881;
--gray: #AA9881;
--gray-100: #817261;
--gray-200: #433434;
--gray-300: #433434;
--gray-400: #AA9881;
--gray-500: #F0E0D6;
--gray-600: #FFE;
--gray-700: #F0E0D6;
--gray-800: #F0E0D6;
--gray-900: #F0E0D6;
--background: #F0E0D6;
}
.text-muted {
color: var(--black) !important;
}
.text-white {
color: var(--primary) !important;
}
.modal .comment-actions a {
color: var(--gray-500);
}
.navbar-light .navbar-nav .nav-link .fa, .navbar-light .navbar-nav .nav-link .fas, .navbar-light .navbar-nav .nav-link .far, .navbar-light .navbar-nav .nav-link .fab {
color: var(--black);
}
.dropdown-item:hover, .dropdown-item:focus, .dropdown-item.active {
background-color: #bda78e !important;
}
html * {
font-family: arial, helvetica, sans-serif;
color: #800000
}
body, #main-content-col {
background-color: var(--secondary) !important;
}
.border-right, .border-left, .border-top, .border-bottom, .border {
border-color: #D9BFB7 !important
}
.card-header {
border-color: #D9BFB7 !important
}
.post-title a {
color: #cc1105 !important;
}
.comment, .card {
background-color: var(--secondary) !important;
border-color: #D9BFB7 !important;
}
.form-control {
border-color: #D9BFB7 !important;
}
.post-actions *:hover, .comment-actions *:hover, .comment-actions .copy-link:hover {
color: #F00 !important
}
a {
color: #00e;
}
blockquote p {
color: #789922 !important;
}
.transparent {
background: None !important;
}
.btn {
color: var(--primary) !important;
}
#profilestuff > * {
color: #cfcfcf !important;
}
.comment {
background-color: var(--background) !important;
}
.navbar-light, .navbar .container-fluid, #mobile-bottom-navigation-bar {
background:#ffe url(https://i.imgur.com/kAfDGxF.png);
background-size: contain;
}
blockquote {
background-color: transparent;
border-color: #789922 !important;
border-width: 1px !important;
}
blockquote a {
color: navy;
}
.comment-collapse-desktop {
border-color: maroon !important;
border-width: 1px !important;
}
.score-up, .active.arrow-up::before, .arrow-up::after, .arrow-up:hover::before {
color: maroon !important;
}
.score-down, .active.arrow-down::before, .arrow-down::after, .arrow-down:hover::before {
color: maroon !important;
}
.arrow-up::before, .arrow-down::before, .score {
color: var(--gray-400);
}
.srd {
background-color: var(--background) !important;
}
.srd a {
color: maroon !important;
}
:root {
--primary: maroon;
}
.caction {
color: var(--gray-400) !important;
}
.user-name span {
color: #117743 !important;
}
.comment-anchor:target, .unread {
background: #ffffff88 !important;
}
#frontpage .post-title a:visited, .visited {
color: #949494 !important;
}

View file

@ -1,103 +1,103 @@
@charset "UTF-8";
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: url(../fonts/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: url(../fonts/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: url(../fonts/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: url(../fonts/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
--dark: #3E2C18;
--secondary: #DDD2C4;
--white: #F1E7D0;
--black: #433434;
--light: #DDD2C4;
--muted: #AA9881;
--gray: #AA9881;
--gray-100: #817261;
--gray-200: #433434;
--gray-300: #433434;
--gray-400: #AA9881;
--gray-500: #DDD2C4;
--gray-600: #DDD2C4;
--gray-700: #DDD2C4;
--gray-800: #DDD2C4;
--gray-900: #DDD2C4;
--background: #DDD2C4;
}
* {
font-family: 'Lato';
}
blockquote {
background-color: #c0c0c0;
}
.text-muted {
color: var(--black) !important;
}
.modal .comment-actions a {
color: var(--gray-500);
}
.navbar-light .navbar-nav .nav-link .fa, .navbar-light .navbar-nav .nav-link .fas, .navbar-light .navbar-nav .nav-link .far, .navbar-light .navbar-nav .nav-link .fab {
color: var(--black);
}
.dropdown-item:hover, .dropdown-item:focus, .dropdown-item.active {
background-color: #bda78e !important;
}
.transparent {
background: None !important;
}
.btn {
color: var(--primary) !important;
}
.table {
color: #433434 !important;
}
#profilestuff > * {
color: #cfcfcf !important;
}
.comment-anchor:target, .unread {
background: #ffffff88 !important;
}
#frontpage .post-title a:visited, .visited {
color: #949494 !important;
@charset "UTF-8";
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: url(../fonts/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: url(../fonts/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: url(../fonts/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: url(../fonts/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
--dark: #3E2C18;
--secondary: #DDD2C4;
--white: #F1E7D0;
--black: #433434;
--light: #DDD2C4;
--muted: #AA9881;
--gray: #AA9881;
--gray-100: #817261;
--gray-200: #433434;
--gray-300: #433434;
--gray-400: #AA9881;
--gray-500: #DDD2C4;
--gray-600: #DDD2C4;
--gray-700: #DDD2C4;
--gray-800: #DDD2C4;
--gray-900: #DDD2C4;
--background: #DDD2C4;
}
* {
font-family: 'Lato';
}
blockquote {
background-color: #c0c0c0;
}
.text-muted {
color: var(--black) !important;
}
.modal .comment-actions a {
color: var(--gray-500);
}
.navbar-light .navbar-nav .nav-link .fa, .navbar-light .navbar-nav .nav-link .fas, .navbar-light .navbar-nav .nav-link .far, .navbar-light .navbar-nav .nav-link .fab {
color: var(--black);
}
.dropdown-item:hover, .dropdown-item:focus, .dropdown-item.active {
background-color: #bda78e !important;
}
.transparent {
background: None !important;
}
.btn {
color: var(--primary) !important;
}
.table {
color: #433434 !important;
}
#profilestuff > * {
color: #cfcfcf !important;
}
.comment-anchor:target, .unread {
background: #ffffff88 !important;
}
#frontpage .post-title a:visited, .visited {
color: #949494 !important;
}

View file

@ -1,72 +1,72 @@
@charset "UTF-8";
:root {
--dark: #383838;
--secondary: #383838;
--white: #E1E1E1;
--black: #CFCFCF;
--light: #000000;
--muted: #E1E1E1;
--gray: #383838;
--gray-100: #E1E1E1;
--gray-200: #E1E1E1;
--gray-300: #383838;
--gray-400: #303030;
--gray-500: #000000;
--gray-600: #000000;
--gray-700: #000000;
--gray-800: #000000;
--gray-900: #000000;
--background: #000000;
}
* {
border-color: var(--primary);
}
.border {
border-color: var(--primary) !important;
}
.form-control {
background: transparent;
border-color: var(--primary) !important;
}
.btn {
background: transparent;
border-color: var(--primary) !important;
}
.form-control:disabled, .form-control[readonly] {
background: transparent;
border-color: var(--primary) !important;
}
.btn-success {
border-color: #38A169 !important;
}
.btn-danger {
border-color: #E53E3E !important;
}
#frontpage .pseudo-submit-form.card .card-body .form-control {
border-color: transparent !important;
}
.btn-lg {
border-color: transparent !important;
}
pre {
color: #CFCFCF;
}
.transparent {
background: None !important;
}
#frontpage .post-title a:visited, .visited {
color: #7a7a7a !important;
@charset "UTF-8";
:root {
--dark: #383838;
--secondary: #383838;
--white: #E1E1E1;
--black: #CFCFCF;
--light: #000000;
--muted: #E1E1E1;
--gray: #383838;
--gray-100: #E1E1E1;
--gray-200: #E1E1E1;
--gray-300: #383838;
--gray-400: #303030;
--gray-500: #000000;
--gray-600: #000000;
--gray-700: #000000;
--gray-800: #000000;
--gray-900: #000000;
--background: #000000;
}
* {
border-color: var(--primary);
}
.border {
border-color: var(--primary) !important;
}
.form-control {
background: transparent;
border-color: var(--primary) !important;
}
.btn {
background: transparent;
border-color: var(--primary) !important;
}
.form-control:disabled, .form-control[readonly] {
background: transparent;
border-color: var(--primary) !important;
}
.btn-success {
border-color: #38A169 !important;
}
.btn-danger {
border-color: #E53E3E !important;
}
#frontpage .pseudo-submit-form.card .card-body .form-control {
border-color: transparent !important;
}
.btn-lg {
border-color: transparent !important;
}
pre {
color: #CFCFCF;
}
.transparent {
background: None !important;
}
#frontpage .post-title a:visited, .visited {
color: #7a7a7a !important;
}

View file

@ -1,85 +1,85 @@
@charset "UTF-8";
:root {
--dark: #c7c7c7;
--secondary: #c7c7c7;
--gray: #c7c7c7;
--gray-300: #c7c7c7;
--gray-400: #cfcfcf;
--gray-500: #ffffff;
--gray-600: #ffffff;
--gray-700: #ffffff;
--gray-800: #ffffff;
--gray-900: #ffffff;
--light: #ffffff;
--muted: #1e1e1e;
--gray-100: #1e1e1e;
--gray-200: #1e1e1e;
--white: #1e1e1e;
--black: #303030;
--background: #ffffff;
}
* {
border-color: var(--primary);
}
.border {
border-color: var(--primary) !important;
}
.form-control {
background: transparent;
border-color: var(--primary) !important;
}
.btn {
background: transparent;
border-color: var(--primary) !important;
}
.form-control:disabled, .form-control[readonly] {
background: transparent;
border-color: var(--primary) !important;
}
.btn-success {
border-color: #38A169 !important;
}
.btn-danger {
border-color: #E53E3E !important;
}
#frontpage .pseudo-submit-form.card .card-body .form-control {
border-color: transparent !important;
}
.btn-lg {
border-color: transparent !important;
}
pre {
color: #CFCFCF;
}
.transparent {
background: None !important;
}
blockquote {
color: var(--gray-400);
}
#profilestuff > * {
color: var(--gray-400) !important;
}
.comment-anchor:target, .unread {
background: #00000055 !important;
}
#frontpage .post-title a:visited, .visited {
color: #7a7a7a !important;
@charset "UTF-8";
:root {
--dark: #c7c7c7;
--secondary: #c7c7c7;
--gray: #c7c7c7;
--gray-300: #c7c7c7;
--gray-400: #cfcfcf;
--gray-500: #ffffff;
--gray-600: #ffffff;
--gray-700: #ffffff;
--gray-800: #ffffff;
--gray-900: #ffffff;
--light: #ffffff;
--muted: #1e1e1e;
--gray-100: #1e1e1e;
--gray-200: #1e1e1e;
--white: #1e1e1e;
--black: #303030;
--background: #ffffff;
}
* {
border-color: var(--primary);
}
.border {
border-color: var(--primary) !important;
}
.form-control {
background: transparent;
border-color: var(--primary) !important;
}
.btn {
background: transparent;
border-color: var(--primary) !important;
}
.form-control:disabled, .form-control[readonly] {
background: transparent;
border-color: var(--primary) !important;
}
.btn-success {
border-color: #38A169 !important;
}
.btn-danger {
border-color: #E53E3E !important;
}
#frontpage .pseudo-submit-form.card .card-body .form-control {
border-color: transparent !important;
}
.btn-lg {
border-color: transparent !important;
}
pre {
color: #CFCFCF;
}
.transparent {
background: None !important;
}
blockquote {
color: var(--gray-400);
}
#profilestuff > * {
color: var(--gray-400) !important;
}
.comment-anchor:target, .unread {
background: #00000055 !important;
}
#frontpage .post-title a:visited, .visited {
color: #7a7a7a !important;
}

File diff suppressed because one or more lines are too long

View file

@ -1,47 +1,47 @@
@charset "UTF-8";
:root {
--dark: #505961;
--secondary: #505961;
--white: #c9d1d9;
--black: #c9d1d9;
--light: #b1bac4;
--muted: #c9d1d9;
--gray: #505961;
--gray-100: #c9d1d9;
--gray-200: #b1bac4;
--gray-300: #505961;
--gray-400: #6e7681;
--gray-500: #21262d;
--gray-600: #21262d;
--gray-700: #21262d;
--gray-800: #161b22;
--gray-900: #0d1117;
--background: #21262d;
}
body, .navbar-light, .navbar-dark, .card, .modal-content, .comment-write textarea {
background-color: var(--gray-600) !important;
}
.transparent, #login {
background: None !important;
}
.tooltip-inner {
color: #383838 !important;
}
.modal .comment-actions .list-group-item {
background-color: var(--gray-600)!important;
}
.page-link {
background-color: #ccc;
}
#frontpage .post-title a:visited, .visited {
color: #6e6e6e !important;
@charset "UTF-8";
:root {
--dark: #505961;
--secondary: #505961;
--white: #c9d1d9;
--black: #c9d1d9;
--light: #b1bac4;
--muted: #c9d1d9;
--gray: #505961;
--gray-100: #c9d1d9;
--gray-200: #b1bac4;
--gray-300: #505961;
--gray-400: #6e7681;
--gray-500: #21262d;
--gray-600: #21262d;
--gray-700: #21262d;
--gray-800: #161b22;
--gray-900: #0d1117;
--background: #21262d;
}
body, .navbar-light, .navbar-dark, .card, .modal-content, .comment-write textarea {
background-color: var(--gray-600) !important;
}
.transparent, #login {
background: None !important;
}
.tooltip-inner {
color: #383838 !important;
}
.modal .comment-actions .list-group-item {
background-color: var(--gray-600)!important;
}
.page-link {
background-color: #ccc;
}
#frontpage .post-title a:visited, .visited {
color: #6e6e6e !important;
}

View file

@ -1,76 +1,76 @@
@charset "UTF-8";
:root {
--dark: #383838;
--secondary: #383838;
--white: #E1E1E1;
--black: #CFCFCF;
--light: transparent;
--muted: #E1E1E1;
--gray: #383838;
--gray-100: #E1E1E1;
--gray-200: #E1E1E1;
--gray-300: #383838;
--gray-400: #303030;
--gray-500: #21262d;
--gray-600: transparent;
--gray-700: transparent;
--gray-800: transparent;
--gray-900: transparent;
--background: #21262d;
}
* {
border-color: var(--primary);
}
.border {
border-color: var(--primary) !important;
}
.form-control {
border-color: var(--primary) !important;
}
.btn {
border-color: var(--primary) !important;
}
.form-control:disabled, .form-control[readonly] {
border-color: var(--primary) !important;
}
.btn-success {
border-color: #38A169 !important;
}
.btn-danger {
border-color: #E53E3E !important;
}
pre {
color: #CFCFCF;
}
.container {
background: rgba(28, 34, 41, 0.90) !important;
border-radius: 0 !important;
}
.dropdown-menu {
background-color: var(--gray-500);
}
.form-inline.search .form-control:active, .form-inline.search .form-control:focus {
background-color: var(--gray-500);
}
.form-control:focus, .form-control:active {
background-color: var(--gray-500);
}
#frontpage .post-title a:visited, .visited {
color: #7a7a7a !important;
@charset "UTF-8";
:root {
--dark: #383838;
--secondary: #383838;
--white: #E1E1E1;
--black: #CFCFCF;
--light: transparent;
--muted: #E1E1E1;
--gray: #383838;
--gray-100: #E1E1E1;
--gray-200: #E1E1E1;
--gray-300: #383838;
--gray-400: #303030;
--gray-500: #21262d;
--gray-600: transparent;
--gray-700: transparent;
--gray-800: transparent;
--gray-900: transparent;
--background: #21262d;
}
* {
border-color: var(--primary);
}
.border {
border-color: var(--primary) !important;
}
.form-control {
border-color: var(--primary) !important;
}
.btn {
border-color: var(--primary) !important;
}
.form-control:disabled, .form-control[readonly] {
border-color: var(--primary) !important;
}
.btn-success {
border-color: #38A169 !important;
}
.btn-danger {
border-color: #E53E3E !important;
}
pre {
color: #CFCFCF;
}
.container {
background: rgba(28, 34, 41, 0.90) !important;
border-radius: 0 !important;
}
.dropdown-menu {
background-color: var(--gray-500);
}
.form-inline.search .form-control:active, .form-inline.search .form-control:focus {
background-color: var(--gray-500);
}
.form-control:focus, .form-control:active {
background-color: var(--gray-500);
}
#frontpage .post-title a:visited, .visited {
color: #7a7a7a !important;
}

View file

@ -1,224 +1,224 @@
@charset "UTF-8";
/* cyrillic-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSV0mf0h.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSx0mf0h.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSt0mf0h.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSd0mf0h.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSZ0mf0h.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSV0mf0h.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSx0mf0h.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSt0mf0h.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSd0mf0h.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSZ0mf0h.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSV0mf0h.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSx0mf0h.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSt0mf0h.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSd0mf0h.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSZ0mf0h.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
--dark: #00001f;
--secondary: var(--primary);
--white: #f0f0f0;
--black: #f0f0f0;
--light: #00001f;
--muted: var(--primary);
--gray: var(--primary);
--gray-100: var(--primary);
--gray-200: var(--primary);
--gray-300: #00001f;
--gray-400: #var(--primary);
--gray-500: #00001f;
--gray-600: #00001f;
--gray-700: #00001f;
--gray-800: #00001f;
--gray-900: #00001f;
--background: #00001f;
}
.comment-box {
border: 2px solid var(--gray-200);
}
.btn {
color: var(--primary);
}
* {
font-family: 'Roboto Mono';
}
.arrow-up::before {
color: var(--white);
}
.arrow-down::before {
color: var(--white);
}
.modal-content {
background-color: var(--gray-300);
}
.modal-header {
background-color: var(--gray-300);
}
.modal .comment-actions .list-group-item {
background-color: var(--gray-300);
}
.comment-actions .score {
color: var(--white);
}
.transparent {
background: None !important;
}
.btn-danger.disabled, .btn-danger:disabled {
color: var(--primary) !important;
}
.followsyou {
background-color: var(--dark) !important;
border: 1px solid var(--primary) !important;
}
#frontpage .pseudo-submit-form.card, .form-inline.search .form-control, .form-control[readonly] {
border: 2px solid var(--gray-200) !important;
}
#frontpage .post-title a:visited, .visited {
color: #b0b0b0 !important;
@charset "UTF-8";
/* cyrillic-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSV0mf0h.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSx0mf0h.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSt0mf0h.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSd0mf0h.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSZ0mf0h.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSV0mf0h.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSx0mf0h.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSt0mf0h.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSd0mf0h.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSZ0mf0h.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 600;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSV0mf0h.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSx0mf0h.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSt0mf0h.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSd0mf0h.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSZ0mf0h.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
src: url(../fonts/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
--dark: #00001f;
--secondary: var(--primary);
--white: #f0f0f0;
--black: #f0f0f0;
--light: #00001f;
--muted: var(--primary);
--gray: var(--primary);
--gray-100: var(--primary);
--gray-200: var(--primary);
--gray-300: #00001f;
--gray-400: #var(--primary);
--gray-500: #00001f;
--gray-600: #00001f;
--gray-700: #00001f;
--gray-800: #00001f;
--gray-900: #00001f;
--background: #00001f;
}
.comment-box {
border: 2px solid var(--gray-200);
}
.btn {
color: var(--primary);
}
* {
font-family: 'Roboto Mono';
}
.arrow-up::before {
color: var(--white);
}
.arrow-down::before {
color: var(--white);
}
.modal-content {
background-color: var(--gray-300);
}
.modal-header {
background-color: var(--gray-300);
}
.modal .comment-actions .list-group-item {
background-color: var(--gray-300);
}
.comment-actions .score {
color: var(--white);
}
.transparent {
background: None !important;
}
.btn-danger.disabled, .btn-danger:disabled {
color: var(--primary) !important;
}
.followsyou {
background-color: var(--dark) !important;
border: 1px solid var(--primary) !important;
}
#frontpage .pseudo-submit-form.card, .form-inline.search .form-control, .form-control[readonly] {
border: 2px solid var(--gray-200) !important;
}
#frontpage .post-title a:visited, .visited {
color: #b0b0b0 !important;
}

View file

@ -1,165 +1,165 @@
@charset "UTF-8";
:root {
--dark: #fff;
--secondary: #000000;
--white: #c0c0c0;
--black: #000000;
--light: #000000;
--muted: #404040;
--gray: #fff;
--gray-100: #394552;
--gray-200: #c0c0c0;
--gray-300: #c0c0c0;
--gray-400: #242C33;
--gray-500: #c0c0c0;
--gray-600: #c0c0c0;
--gray-700: #c0c0c0;
--gray-800: #07090A;
--gray-900: #000000;
--background: #c0c0c0;
}
* {
box-shadow: none !important;
transition: none !important;
border-radius: 0 !important;
font-family: sans-serif;
font-size: 12px;
}
.card, #mobile-bottom-navigation-bar {
transform: none !important;
background-color: var(--gray-500) !important;
}
.pseudo-submit-form .card-header, .card-blank .card-header, .navbar-light, .nav, .flex-row.bg-gray-200.sticky.justify-content-center.d-flex.d-sm-none.mt-3.mb-3.mb-sm-5.rounded {
background: var(--white) !important;
color: var(--white) !important;
}
.navbar-light > *, .navbar-brand, .nav-link{
color: var(--white) !important;
}
.nav-link, .btn:not(.caction), .btn-secondary, .col.px-0.pl-2.btn.btn-dead.my-0.mx-2 {
border: 2px outset white !important;
border-bottom: 2px solid #a7a5a1 !important;
border-right: 2px solid #a7a5a1 !important;
background: var(--gray-500) !important;
color: #000 !important;
}
.nav-link:focus, .btn:focus, .btn-secondary:focus, .col.px-0.pl-2.btn.btn-dead.my-0.mx-2,
.nav-link:active, .btn:active, .btn-secondary:active {
border: 2px inset white !important;
}
.card, .card-blank, .comment-section.post-top, .dropdown-menu {
border: 2px outset white !important;
border-bottom: 2px solid #a7a5a1 !important;
border-right: 2px solid #a7a5a1 !important;
}
.dropdown-menu, .btn:hover {
background: var(--gray-500) !important;
color: #000 !important;
}
.form-control {
border: 2px inset #717171 !important;
border-bottom: 2px solid #a7a5a1 !important;
border-right: 2px solid #a7a5a1 !important;
}
.post-title, .text-small-extra.text-muted, #actual-comments > .text-muted, .text-left.pl-2 {
color: var(--black) !important;
}
.arrow-up::before {
color: var(--muted);
}
blockquote {
background-color: #e0e0e0 !important;
border-bottom: 2px solid var(--black);
}
#frontpage .pseudo-submit-form.card .card-body .form-control {
background-color: white !important;
}
.navbar-light .navbar-nav .nav-link .fa, .navbar-light .navbar-nav .nav-link .fas, .navbar-light .navbar-nav .nav-link .far, .navbar-light .navbar-nav .nav-link .fab {
color: #000 !important
}
.stretched-link {
color: #000 !important
}
.flaggers {
background-color: var(--white) !important;
}
.form-control, .form-control:disabled, .form-control[readonly] {
background: white !important;
color: black !important
}
.arrow-down::before {
color: var(--muted) !important;
}
.table {
color: black !important;
}
.transparent {
background: None !important;
}
.black {
color: black !important;
}
.col-12 .card {
padding: 20px !important;
}
.post-actions a, .post-actions button {
color: var(--muted) !important;
}
.modal-header, .modal .comment-actions a, .modal-title {
color: var(--white) !important;
}
.text-info
{
color: var(--primary) !important;
}
.score, .comment-actions .score {
color: #000;
}
.arrow-up:hover::before {
color: var(--muted);
}
#thread {
background: var(--white) !important;
}
#profilestuff > * {
color: #cfcfcf !important;
}
.comment-anchor:target, .unread {
background: #ffffffaa !important;
}
#frontpage .post-title a:visited, .visited {
color: #5c5c5c !important;
@charset "UTF-8";
:root {
--dark: #fff;
--secondary: #000000;
--white: #c0c0c0;
--black: #000000;
--light: #000000;
--muted: #404040;
--gray: #fff;
--gray-100: #394552;
--gray-200: #c0c0c0;
--gray-300: #c0c0c0;
--gray-400: #242C33;
--gray-500: #c0c0c0;
--gray-600: #c0c0c0;
--gray-700: #c0c0c0;
--gray-800: #07090A;
--gray-900: #000000;
--background: #c0c0c0;
}
* {
box-shadow: none !important;
transition: none !important;
border-radius: 0 !important;
font-family: sans-serif;
font-size: 12px;
}
.card, #mobile-bottom-navigation-bar {
transform: none !important;
background-color: var(--gray-500) !important;
}
.pseudo-submit-form .card-header, .card-blank .card-header, .navbar-light, .nav, .flex-row.bg-gray-200.sticky.justify-content-center.d-flex.d-sm-none.mt-3.mb-3.mb-sm-5.rounded {
background: var(--white) !important;
color: var(--white) !important;
}
.navbar-light > *, .navbar-brand, .nav-link{
color: var(--white) !important;
}
.nav-link, .btn:not(.caction), .btn-secondary, .col.px-0.pl-2.btn.btn-dead.my-0.mx-2 {
border: 2px outset white !important;
border-bottom: 2px solid #a7a5a1 !important;
border-right: 2px solid #a7a5a1 !important;
background: var(--gray-500) !important;
color: #000 !important;
}
.nav-link:focus, .btn:focus, .btn-secondary:focus, .col.px-0.pl-2.btn.btn-dead.my-0.mx-2,
.nav-link:active, .btn:active, .btn-secondary:active {
border: 2px inset white !important;
}
.card, .card-blank, .comment-section.post-top, .dropdown-menu {
border: 2px outset white !important;
border-bottom: 2px solid #a7a5a1 !important;
border-right: 2px solid #a7a5a1 !important;
}
.dropdown-menu, .btn:hover {
background: var(--gray-500) !important;
color: #000 !important;
}
.form-control {
border: 2px inset #717171 !important;
border-bottom: 2px solid #a7a5a1 !important;
border-right: 2px solid #a7a5a1 !important;
}
.post-title, .text-small-extra.text-muted, #actual-comments > .text-muted, .text-left.pl-2 {
color: var(--black) !important;
}
.arrow-up::before {
color: var(--muted);
}
blockquote {
background-color: #e0e0e0 !important;
border-bottom: 2px solid var(--black);
}
#frontpage .pseudo-submit-form.card .card-body .form-control {
background-color: white !important;
}
.navbar-light .navbar-nav .nav-link .fa, .navbar-light .navbar-nav .nav-link .fas, .navbar-light .navbar-nav .nav-link .far, .navbar-light .navbar-nav .nav-link .fab {
color: #000 !important
}
.stretched-link {
color: #000 !important
}
.flaggers {
background-color: var(--white) !important;
}
.form-control, .form-control:disabled, .form-control[readonly] {
background: white !important;
color: black !important
}
.arrow-down::before {
color: var(--muted) !important;
}
.table {
color: black !important;
}
.transparent {
background: None !important;
}
.black {
color: black !important;
}
.col-12 .card {
padding: 20px !important;
}
.post-actions a, .post-actions button {
color: var(--muted) !important;
}
.modal-header, .modal .comment-actions a, .modal-title {
color: var(--white) !important;
}
.text-info
{
color: var(--primary) !important;
}
.score, .comment-actions .score {
color: #000;
}
.arrow-up:hover::before {
color: var(--muted);
}
#thread {
background: var(--white) !important;
}
#profilestuff > * {
color: #cfcfcf !important;
}
.comment-anchor:target, .unread {
background: #ffffffaa !important;
}
#frontpage .post-title a:visited, .visited {
color: #5c5c5c !important;
}

View file

@ -1,151 +1,151 @@
document.getElementById('awardModal').addEventListener('show.bs.modal', function (event) {
document.getElementById("awardTarget").action = event.relatedTarget.dataset.url;
});
function vote(type, id, dir, vid) {
const upvotes = document.getElementsByClassName(type + '-' + id + '-up');
const downvotes = document.getElementsByClassName(type + '-' + id + '-down');
const scoretexts = document.getElementsByClassName(type + '-score-' + id);
for (let i=0; i<upvotes.length; i++) {
const upvote = upvotes[i]
const downvote = downvotes[i]
const scoretext = scoretexts[i]
const score = Number(scoretext.textContent);
if (dir == "1") {
if (upvote.classList.contains('active')) {
upvote.classList.remove('active')
scoretext.textContent = score - 1
votedirection = "0"
if (vid && ['1','9'].includes(vid)) document.getElementById(id+'-title').classList.remove('visited')
} else if (downvote.classList.contains('active')) {
upvote.classList.add('active')
downvote.classList.remove('active')
scoretext.textContent = score + 2
votedirection = "1"
} else {
upvote.classList.add('active')
scoretext.textContent = score + 1
votedirection = "1"
if (vid && ['1','9'].includes(vid)) document.getElementById(id+'-title').classList.add('visited')
}
if (upvote.classList.contains('active')) {
scoretext.classList.add('score-up')
scoretext.classList.remove('score-down')
scoretext.classList.remove('score')
} else if (downvote.classList.contains('active')) {
scoretext.classList.add('score-down')
scoretext.classList.remove('score-up')
scoretext.classList.remove('score')
} else {
scoretext.classList.add('score')
scoretext.classList.remove('score-up')
scoretext.classList.remove('score-down')
}
}
else {
if (downvote.classList.contains('active')) {
downvote.classList.remove('active')
scoretext.textContent = score + 1
votedirection = "0"
if (vid && ['1','9'].includes(vid)) document.getElementById(id+'-title').classList.remove('visited')
} else if (upvote.classList.contains('active')) {
downvote.classList.add('active')
upvote.classList.remove('active')
scoretext.textContent = score - 2
votedirection = "-1"
} else {
downvote.classList.add('active')
scoretext.textContent = score - 1
votedirection = "-1"
if (vid && ['1','9'].includes(vid)) document.getElementById(id+'-title').classList.add('visited')
}
if (upvote.classList.contains('active')) {
scoretext.classList.add('score-up')
scoretext.classList.remove('score-down')
scoretext.classList.remove('score')
} else if (downvote.classList.contains('active')) {
scoretext.classList.add('score-down')
scoretext.classList.remove('score-up')
scoretext.classList.remove('score')
} else {
scoretext.classList.add('score')
scoretext.classList.remove('score-up')
scoretext.classList.remove('score-down')
}
}
}
const xhr = new XMLHttpRequest();
xhr.open("POST", "/vote/" + type.replace('-mobile','') + "/" + id + "/" + votedirection);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
xhr.send(form);
}
function pick(kind, canbuy1, canbuy2) {
let buy1 = document.getElementById('buy1')
if (canbuy1 && kind != "grass") buy1.disabled=false;
else buy1.disabled=true;
let buy2 = document.getElementById('buy2')
if (canbuy2 && kind != "benefactor") buy2.disabled=false;
else buy2.disabled=true;
let ownednum = Number(document.getElementById(`${kind}-owned`).textContent);
document.getElementById('giveaward').disabled = (ownednum == 0);
document.getElementById('kind').value=kind;
try {document.getElementsByClassName('picked')[0].classList.toggle('picked');} catch(e) {console.log(e)}
document.getElementById(kind).classList.toggle('picked')
if (kind == "flairlock") {
document.getElementById('notelabel').innerHTML = "New flair:";
document.getElementById('note').placeholder = "Insert new flair here, or leave empty to add 1 day to the duration of the current flair";
}
else {
document.getElementById('notelabel').innerHTML = "Note (optional):";
document.getElementById('note').placeholder = "Note to include in award notification";
}
}
function buy(mb) {
const kind = document.getElementById('kind').value;
const xhr = new XMLHttpRequest();
url = `/buy/${kind}`
if (mb) url += "?mb=true"
xhr.open("POST", url);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
if(typeof data === 'object' && data !== null) {
for(let k of Object.keys(data)) {
form.append(k, data[k]);
}
}
form.append("formkey", formkey());
xhr.onload = function() {
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (xhr.status >= 200 && xhr.status < 300 && data && data["message"]) {
document.getElementById('toast-post-success-text2').innerText = data["message"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-success2')).show();
document.getElementById('giveaward').disabled=false;
let owned = document.getElementById(`${kind}-owned`)
let ownednum = Number(owned.textContent);
owned.textContent = ownednum + 1
} else {
document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
if (data && data["error"]) document.getElementById('toast-post-error-text2').innerText = data["error"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error2')).show();
}
};
xhr.send(form);
document.getElementById('awardModal').addEventListener('show.bs.modal', function (event) {
document.getElementById("awardTarget").action = event.relatedTarget.dataset.url;
});
function vote(type, id, dir, vid) {
const upvotes = document.getElementsByClassName(type + '-' + id + '-up');
const downvotes = document.getElementsByClassName(type + '-' + id + '-down');
const scoretexts = document.getElementsByClassName(type + '-score-' + id);
for (let i=0; i<upvotes.length; i++) {
const upvote = upvotes[i]
const downvote = downvotes[i]
const scoretext = scoretexts[i]
const score = Number(scoretext.textContent);
if (dir == "1") {
if (upvote.classList.contains('active')) {
upvote.classList.remove('active')
scoretext.textContent = score - 1
votedirection = "0"
if (vid && ['1','9'].includes(vid)) document.getElementById(id+'-title').classList.remove('visited')
} else if (downvote.classList.contains('active')) {
upvote.classList.add('active')
downvote.classList.remove('active')
scoretext.textContent = score + 2
votedirection = "1"
} else {
upvote.classList.add('active')
scoretext.textContent = score + 1
votedirection = "1"
if (vid && ['1','9'].includes(vid)) document.getElementById(id+'-title').classList.add('visited')
}
if (upvote.classList.contains('active')) {
scoretext.classList.add('score-up')
scoretext.classList.remove('score-down')
scoretext.classList.remove('score')
} else if (downvote.classList.contains('active')) {
scoretext.classList.add('score-down')
scoretext.classList.remove('score-up')
scoretext.classList.remove('score')
} else {
scoretext.classList.add('score')
scoretext.classList.remove('score-up')
scoretext.classList.remove('score-down')
}
}
else {
if (downvote.classList.contains('active')) {
downvote.classList.remove('active')
scoretext.textContent = score + 1
votedirection = "0"
if (vid && ['1','9'].includes(vid)) document.getElementById(id+'-title').classList.remove('visited')
} else if (upvote.classList.contains('active')) {
downvote.classList.add('active')
upvote.classList.remove('active')
scoretext.textContent = score - 2
votedirection = "-1"
} else {
downvote.classList.add('active')
scoretext.textContent = score - 1
votedirection = "-1"
if (vid && ['1','9'].includes(vid)) document.getElementById(id+'-title').classList.add('visited')
}
if (upvote.classList.contains('active')) {
scoretext.classList.add('score-up')
scoretext.classList.remove('score-down')
scoretext.classList.remove('score')
} else if (downvote.classList.contains('active')) {
scoretext.classList.add('score-down')
scoretext.classList.remove('score-up')
scoretext.classList.remove('score')
} else {
scoretext.classList.add('score')
scoretext.classList.remove('score-up')
scoretext.classList.remove('score-down')
}
}
}
const xhr = new XMLHttpRequest();
xhr.open("POST", "/vote/" + type.replace('-mobile','') + "/" + id + "/" + votedirection);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
xhr.send(form);
}
function pick(kind, canbuy1, canbuy2) {
let buy1 = document.getElementById('buy1')
if (canbuy1 && kind != "grass") buy1.disabled=false;
else buy1.disabled=true;
let buy2 = document.getElementById('buy2')
if (canbuy2 && kind != "benefactor") buy2.disabled=false;
else buy2.disabled=true;
let ownednum = Number(document.getElementById(`${kind}-owned`).textContent);
document.getElementById('giveaward').disabled = (ownednum == 0);
document.getElementById('kind').value=kind;
try {document.getElementsByClassName('picked')[0].classList.toggle('picked');} catch(e) {console.log(e)}
document.getElementById(kind).classList.toggle('picked')
if (kind == "flairlock") {
document.getElementById('notelabel').innerHTML = "New flair:";
document.getElementById('note').placeholder = "Insert new flair here, or leave empty to add 1 day to the duration of the current flair";
}
else {
document.getElementById('notelabel').innerHTML = "Note (optional):";
document.getElementById('note').placeholder = "Note to include in award notification";
}
}
function buy(mb) {
const kind = document.getElementById('kind').value;
const xhr = new XMLHttpRequest();
url = `/buy/${kind}`
if (mb) url += "?mb=true"
xhr.open("POST", url);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
if(typeof data === 'object' && data !== null) {
for(let k of Object.keys(data)) {
form.append(k, data[k]);
}
}
form.append("formkey", formkey());
xhr.onload = function() {
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (xhr.status >= 200 && xhr.status < 300 && data && data["message"]) {
document.getElementById('toast-post-success-text2').innerText = data["message"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-success2')).show();
document.getElementById('giveaward').disabled=false;
let owned = document.getElementById(`${kind}-owned`)
let ownednum = Number(owned.textContent);
owned.textContent = ownednum + 1
} else {
document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
if (data && data["error"]) document.getElementById('toast-post-error-text2').innerText = data["error"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error2')).show();
}
};
xhr.send(form);
}

View file

@ -1,7 +1,7 @@
new BugController({
imageSprite: "/assets/images/fly-sprite.webp",
canDie: false,
minBugs: 10,
maxBugs: 20,
mouseOver: "multiply"
new BugController({
imageSprite: "/assets/images/fly-sprite.webp",
canDie: false,
minBugs: 10,
maxBugs: 20,
mouseOver: "multiply"
});

View file

@ -1,74 +1,74 @@
function removeComment(post_id,button1,button2) {
url="/ban_comment/"+post_id
post(url)
try {
document.getElementById("comment-"+post_id+"-only").classList.add("banned");
} catch(e) {
document.getElementById("context").classList.add("banned");
}
var button=document.getElementById("remove-"+post_id);
button.onclick=function(){approveComment(post_id)};
button.innerHTML='<i class="fas fa-clipboard-check"></i>Approve'
if (typeof button1 !== 'undefined') {
document.getElementById(button1).classList.toggle("d-md-inline-block");
document.getElementById(button2).classList.toggle("d-md-inline-block");
}
};
function approveComment(post_id,button1,button2) {
url="/unban_comment/"+post_id
post(url)
try {
document.getElementById("comment-"+post_id+"-only").classList.remove("banned");
} catch(e) {
document.getElementById("context").classList.remove("banned");
}
var button=document.getElementById("remove-"+post_id);
button.onclick=function(){removeComment(post_id)};
button.innerHTML='<i class="fas fa-trash-alt"></i>Remove'
if (typeof button1 !== 'undefined') {
document.getElementById(button1).classList.toggle("d-md-inline-block");
document.getElementById(button2).classList.toggle("d-md-inline-block");
}
}
function removeComment2(post_id,button1,button2) {
url="/ban_comment/"+post_id
post(url)
document.getElementById("comment-"+post_id+"-only").classList.add("banned");
var button=document.getElementById("remove-"+post_id);
button.onclick=function(){approveComment(post_id)};
button.innerHTML='<i class="fas fa-clipboard-check"></i>Approve'
if (typeof button1 !== 'undefined') {
document.getElementById(button1).classList.toggle("d-none");
document.getElementById(button2).classList.toggle("d-none");
}
};
function approveComment2(post_id,button1,button2) {
url="/unban_comment/"+post_id
post(url)
document.getElementById("comment-"+post_id+"-only").classList.remove("banned");
var button=document.getElementById("remove-"+post_id);
button.onclick=function(){removeComment(post_id)};
button.innerHTML='<i class="fas fa-trash-alt"></i>Remove'
if (typeof button1 !== 'undefined') {
document.getElementById(button1).classList.toggle("d-none");
document.getElementById(button2).classList.toggle("d-none");
}
function removeComment(post_id,button1,button2) {
url="/ban_comment/"+post_id
post(url)
try {
document.getElementById("comment-"+post_id+"-only").classList.add("banned");
} catch(e) {
document.getElementById("context").classList.add("banned");
}
var button=document.getElementById("remove-"+post_id);
button.onclick=function(){approveComment(post_id)};
button.innerHTML='<i class="fas fa-clipboard-check"></i>Approve'
if (typeof button1 !== 'undefined') {
document.getElementById(button1).classList.toggle("d-md-inline-block");
document.getElementById(button2).classList.toggle("d-md-inline-block");
}
};
function approveComment(post_id,button1,button2) {
url="/unban_comment/"+post_id
post(url)
try {
document.getElementById("comment-"+post_id+"-only").classList.remove("banned");
} catch(e) {
document.getElementById("context").classList.remove("banned");
}
var button=document.getElementById("remove-"+post_id);
button.onclick=function(){removeComment(post_id)};
button.innerHTML='<i class="fas fa-trash-alt"></i>Remove'
if (typeof button1 !== 'undefined') {
document.getElementById(button1).classList.toggle("d-md-inline-block");
document.getElementById(button2).classList.toggle("d-md-inline-block");
}
}
function removeComment2(post_id,button1,button2) {
url="/ban_comment/"+post_id
post(url)
document.getElementById("comment-"+post_id+"-only").classList.add("banned");
var button=document.getElementById("remove-"+post_id);
button.onclick=function(){approveComment(post_id)};
button.innerHTML='<i class="fas fa-clipboard-check"></i>Approve'
if (typeof button1 !== 'undefined') {
document.getElementById(button1).classList.toggle("d-none");
document.getElementById(button2).classList.toggle("d-none");
}
};
function approveComment2(post_id,button1,button2) {
url="/unban_comment/"+post_id
post(url)
document.getElementById("comment-"+post_id+"-only").classList.remove("banned");
var button=document.getElementById("remove-"+post_id);
button.onclick=function(){removeComment(post_id)};
button.innerHTML='<i class="fas fa-trash-alt"></i>Remove'
if (typeof button1 !== 'undefined') {
document.getElementById(button1).classList.toggle("d-none");
document.getElementById(button2).classList.toggle("d-none");
}
}

View file

@ -1,343 +1,343 @@
function post_toast3(t, url, button1, button2) {
t.disabled=true;
t.classList.add("disabled");
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
if(typeof data === 'object' && data !== null) {
for(let k of Object.keys(data)) {
form.append(k, data[k]);
}
}
xhr.onload = function() {
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (xhr.status >= 200 && xhr.status < 300 && data && data["message"]) {
document.getElementById('toast-post-success-text').innerText = data["message"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-success')).show();
document.getElementById(button1).classList.toggle("d-md-inline-block");
document.getElementById(button2).classList.toggle("d-md-inline-block");
} else {
document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
t.disabled = false;
t.classList.remove("disabled");
}, 2000);
};
xhr.send(form);
}
function report_commentModal(id, author) {
document.getElementById("comment-author").textContent = author;
document.getElementById("reportCommentFormBefore").classList.remove('d-none');
document.getElementById("reportCommentFormAfter").classList.add('d-none');
btn = document.getElementById("reportCommentButton")
btn.innerHTML='Report comment';
btn.disabled = false;
btn.classList.remove('disabled');
reason = document.getElementById("reason-comment")
reason.value = ""
btn.onclick = function() {
this.innerHTML='Reporting comment';
this.disabled = true;
this.classList.add('disabled');
const xhr = new XMLHttpRequest();
xhr.open("POST", '/report/comment/'+id);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
form.append("reason", reason.value);
xhr.onload=function() {
document.getElementById("reportCommentFormBefore").classList.add('d-none');
document.getElementById("reportCommentFormAfter").classList.remove('d-none');
};
xhr.onerror=function(){alert(errortext)};
xhr.send(form);
}
};
function openReplyBox(id) {
const element = document.getElementById(id);
const textarea = element.getElementsByTagName('textarea')[0]
let text = getSelection().toString()
if (text)
{
textarea.value = '>' + text
textarea.value = textarea.value.replace(/\n\n([^$])/g,"\n\n>$1")
if (!textarea.value.endsWith('\n\n')) textarea.value += '\n\n'
}
element.classList.remove('d-none')
textarea.focus()
}
function toggleEdit(id){
comment=document.getElementById("comment-text-"+id);
form=document.getElementById("comment-edit-"+id);
box=document.getElementById('comment-edit-body-'+id);
actions = document.getElementById('comment-' + id +'-actions');
comment.classList.toggle("d-none");
form.classList.toggle("d-none");
actions.classList.toggle("d-none");
autoExpand(box);
};
function delete_commentModal(id) {
document.getElementById("deleteCommentButton").onclick = function() {
const xhr = new XMLHttpRequest();
xhr.open("POST", `/delete/comment/${id}`);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
xhr.onload = function() {
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (xhr.status >= 200 && xhr.status < 300 && data && data['message']) {
document.getElementsByClassName(`comment-${id}-only`)[0].classList.add('deleted');
document.getElementById(`delete-${id}`).classList.add('d-none');
document.getElementById(`undelete-${id}`).classList.remove('d-none');
document.getElementById(`delete2-${id}`).classList.add('d-none');
document.getElementById(`undelete2-${id}`).classList.remove('d-none');
document.getElementById('toast-post-success-text').innerText = data["message"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-success')).show();
} else {
document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
};
xhr.send(form);
};
}
function post_reply(id){
const btn = document.getElementById(`save-reply-to-${id}`)
btn.disabled = true;
btn.classList.add('disabled');
var form = new FormData();
form.append('formkey', formkey());
form.append('parent_id', id);
form.append('body', document.getElementById('reply-form-body-'+id).value);
try {
for (const e of document.getElementById('file-upload').files)
form.append('file', e);
}
catch(e) {}
const xhr = new XMLHttpRequest();
xhr.open("post", "/reply");
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["comment"]) {
commentForm=document.getElementById('comment-form-space-'+id);
commentForm.innerHTML = data["comment"].replace(/data-src/g, 'src').replace(/data-cfsrc/g, 'src').replace(/style="display:none;visibility:hidden;"/g, '').replace('comment-collapse-desktop d-none d-md-block','d-none').replace('border-left: 2px solid','padding-left:0;border-left: 0px solid');
bs_trigger(commentForm);
}
else {
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
else document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('disabled');
}, 2000);
}
xhr.send(form)
}
function comment_edit(id){
const btn = document.getElementById(`edit-btn-${id}`)
btn.disabled = true
btn.classList.add('disabled');
var form = new FormData();
form.append('formkey', formkey());
form.append('body', document.getElementById('comment-edit-body-'+id).value);
try {
for (const e of document.getElementById('file-edit-reply-'+id).files)
form.append('file', e);
}
catch(e) {}
const xhr = new XMLHttpRequest();
xhr.open("post", "/edit_comment/"+id);
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["comment"]) {
commentForm=document.getElementById('comment-text-'+id);
commentForm.innerHTML = data["comment"].replace(/data-src/g, 'src').replace(/data-cfsrc/g, 'src').replace(/style="display:none;visibility:hidden;"/g, '')
document.getElementById('cancel-edit-'+id).click()
bs_trigger(commentForm);
}
else {
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
else document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('disabled');
}, 2000);
}
xhr.send(form)
}
function post_comment(fullname){
const btn = document.getElementById('save-reply-to-'+fullname)
btn.disabled = true
btn.classList.add('disabled');
var form = new FormData();
form.append('formkey', formkey());
form.append('parent_fullname', fullname);
form.append('submission', document.getElementById('reply-form-submission-'+fullname).value);
form.append('body', document.getElementById('reply-form-body-'+fullname).value);
try {
for (const e of document.getElementById('file-upload-reply-'+fullname).files)
form.append('file', e);
}
catch(e) {}
const xhr = new XMLHttpRequest();
xhr.open("post", "/comment");
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["comment"]) {
commentForm=document.getElementById('comment-form-space-'+fullname);
commentForm.innerHTML = data["comment"].replace(/data-src/g, 'src').replace(/data-cfsrc/g, 'src').replace(/style="display:none;visibility:hidden;"/g, '');
bs_trigger(commentForm);
}
else {
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
else document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('disabled');
}, 2000);
}
xhr.send(form)
}
document.onpaste = function(event) {
var focused = document.activeElement;
if (focused.id.includes('reply-form-body-')) {
var fullname = focused.dataset.fullname;
f=document.getElementById('file-upload-reply-' + fullname);
files = event.clipboardData.files
try {
filename = files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
f.files = files;
document.getElementById('filename-show-reply-' + fullname).textContent = filename;
}
}
catch(e) {console.log(e)}
}
else if (focused.id.includes('comment-edit-body-')) {
var id = focused.dataset.id;
f=document.getElementById('file-edit-reply-' + id);
files = event.clipboardData.files
filename = files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
f.files = files;
document.getElementById('filename-edit-reply-' + id).textContent = filename;
}
}
else if (focused.id.includes('post-edit-box-')) {
var id = focused.dataset.id;
f=document.getElementById('file-upload-edit-' + id);
files = event.clipboardData.files
filename = files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
f.files = files;
document.getElementById('filename-show-edit-' + id).textContent = filename;
}
}
}
function handle_action(type, cid, thing) {
const btns = document.getElementsByClassName(`action-${cid}`)
for (const btn of btns)
{
btn.disabled = true;
btn.classList.add('disabled');
}
const form = new FormData();
form.append('formkey', formkey());
form.append('comment_id', cid);
form.append('thing', thing);
const xhr = new XMLHttpRequest();
xhr.open("post", `/${type}/${cid}`);
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["response"]) {
const element = document.getElementById(`${type}-${cid}`);
element.innerHTML = data["response"]
}
else {
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
else document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
for (const btn of btns)
{
btn.disabled = false;
btn.classList.remove('disabled');
}
}, 2000);
}
xhr.send(form)
function post_toast3(t, url, button1, button2) {
t.disabled=true;
t.classList.add("disabled");
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
if(typeof data === 'object' && data !== null) {
for(let k of Object.keys(data)) {
form.append(k, data[k]);
}
}
xhr.onload = function() {
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (xhr.status >= 200 && xhr.status < 300 && data && data["message"]) {
document.getElementById('toast-post-success-text').innerText = data["message"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-success')).show();
document.getElementById(button1).classList.toggle("d-md-inline-block");
document.getElementById(button2).classList.toggle("d-md-inline-block");
} else {
document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
t.disabled = false;
t.classList.remove("disabled");
}, 2000);
};
xhr.send(form);
}
function report_commentModal(id, author) {
document.getElementById("comment-author").textContent = author;
document.getElementById("reportCommentFormBefore").classList.remove('d-none');
document.getElementById("reportCommentFormAfter").classList.add('d-none');
btn = document.getElementById("reportCommentButton")
btn.innerHTML='Report comment';
btn.disabled = false;
btn.classList.remove('disabled');
reason = document.getElementById("reason-comment")
reason.value = ""
btn.onclick = function() {
this.innerHTML='Reporting comment';
this.disabled = true;
this.classList.add('disabled');
const xhr = new XMLHttpRequest();
xhr.open("POST", '/report/comment/'+id);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
form.append("reason", reason.value);
xhr.onload=function() {
document.getElementById("reportCommentFormBefore").classList.add('d-none');
document.getElementById("reportCommentFormAfter").classList.remove('d-none');
};
xhr.onerror=function(){alert(errortext)};
xhr.send(form);
}
};
function openReplyBox(id) {
const element = document.getElementById(id);
const textarea = element.getElementsByTagName('textarea')[0]
let text = getSelection().toString()
if (text)
{
textarea.value = '>' + text
textarea.value = textarea.value.replace(/\n\n([^$])/g,"\n\n>$1")
if (!textarea.value.endsWith('\n\n')) textarea.value += '\n\n'
}
element.classList.remove('d-none')
textarea.focus()
}
function toggleEdit(id){
comment=document.getElementById("comment-text-"+id);
form=document.getElementById("comment-edit-"+id);
box=document.getElementById('comment-edit-body-'+id);
actions = document.getElementById('comment-' + id +'-actions');
comment.classList.toggle("d-none");
form.classList.toggle("d-none");
actions.classList.toggle("d-none");
autoExpand(box);
};
function delete_commentModal(id) {
document.getElementById("deleteCommentButton").onclick = function() {
const xhr = new XMLHttpRequest();
xhr.open("POST", `/delete/comment/${id}`);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
xhr.onload = function() {
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (xhr.status >= 200 && xhr.status < 300 && data && data['message']) {
document.getElementsByClassName(`comment-${id}-only`)[0].classList.add('deleted');
document.getElementById(`delete-${id}`).classList.add('d-none');
document.getElementById(`undelete-${id}`).classList.remove('d-none');
document.getElementById(`delete2-${id}`).classList.add('d-none');
document.getElementById(`undelete2-${id}`).classList.remove('d-none');
document.getElementById('toast-post-success-text').innerText = data["message"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-success')).show();
} else {
document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
};
xhr.send(form);
};
}
function post_reply(id){
const btn = document.getElementById(`save-reply-to-${id}`)
btn.disabled = true;
btn.classList.add('disabled');
var form = new FormData();
form.append('formkey', formkey());
form.append('parent_id', id);
form.append('body', document.getElementById('reply-form-body-'+id).value);
try {
for (const e of document.getElementById('file-upload').files)
form.append('file', e);
}
catch(e) {}
const xhr = new XMLHttpRequest();
xhr.open("post", "/reply");
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["comment"]) {
commentForm=document.getElementById('comment-form-space-'+id);
commentForm.innerHTML = data["comment"].replace(/data-src/g, 'src').replace(/data-cfsrc/g, 'src').replace(/style="display:none;visibility:hidden;"/g, '').replace('comment-collapse-desktop d-none d-md-block','d-none').replace('border-left: 2px solid','padding-left:0;border-left: 0px solid');
bs_trigger(commentForm);
}
else {
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
else document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('disabled');
}, 2000);
}
xhr.send(form)
}
function comment_edit(id){
const btn = document.getElementById(`edit-btn-${id}`)
btn.disabled = true
btn.classList.add('disabled');
var form = new FormData();
form.append('formkey', formkey());
form.append('body', document.getElementById('comment-edit-body-'+id).value);
try {
for (const e of document.getElementById('file-edit-reply-'+id).files)
form.append('file', e);
}
catch(e) {}
const xhr = new XMLHttpRequest();
xhr.open("post", "/edit_comment/"+id);
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["comment"]) {
commentForm=document.getElementById('comment-text-'+id);
commentForm.innerHTML = data["comment"].replace(/data-src/g, 'src').replace(/data-cfsrc/g, 'src').replace(/style="display:none;visibility:hidden;"/g, '')
document.getElementById('cancel-edit-'+id).click()
bs_trigger(commentForm);
}
else {
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
else document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('disabled');
}, 2000);
}
xhr.send(form)
}
function post_comment(fullname){
const btn = document.getElementById('save-reply-to-'+fullname)
btn.disabled = true
btn.classList.add('disabled');
var form = new FormData();
form.append('formkey', formkey());
form.append('parent_fullname', fullname);
form.append('submission', document.getElementById('reply-form-submission-'+fullname).value);
form.append('body', document.getElementById('reply-form-body-'+fullname).value);
try {
for (const e of document.getElementById('file-upload-reply-'+fullname).files)
form.append('file', e);
}
catch(e) {}
const xhr = new XMLHttpRequest();
xhr.open("post", "/comment");
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["comment"]) {
commentForm=document.getElementById('comment-form-space-'+fullname);
commentForm.innerHTML = data["comment"].replace(/data-src/g, 'src').replace(/data-cfsrc/g, 'src').replace(/style="display:none;visibility:hidden;"/g, '');
bs_trigger(commentForm);
}
else {
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
else document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('disabled');
}, 2000);
}
xhr.send(form)
}
document.onpaste = function(event) {
var focused = document.activeElement;
if (focused.id.includes('reply-form-body-')) {
var fullname = focused.dataset.fullname;
f=document.getElementById('file-upload-reply-' + fullname);
files = event.clipboardData.files
try {
filename = files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
f.files = files;
document.getElementById('filename-show-reply-' + fullname).textContent = filename;
}
}
catch(e) {console.log(e)}
}
else if (focused.id.includes('comment-edit-body-')) {
var id = focused.dataset.id;
f=document.getElementById('file-edit-reply-' + id);
files = event.clipboardData.files
filename = files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
f.files = files;
document.getElementById('filename-edit-reply-' + id).textContent = filename;
}
}
else if (focused.id.includes('post-edit-box-')) {
var id = focused.dataset.id;
f=document.getElementById('file-upload-edit-' + id);
files = event.clipboardData.files
filename = files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
f.files = files;
document.getElementById('filename-show-edit-' + id).textContent = filename;
}
}
}
function handle_action(type, cid, thing) {
const btns = document.getElementsByClassName(`action-${cid}`)
for (const btn of btns)
{
btn.disabled = true;
btn.classList.add('disabled');
}
const form = new FormData();
form.append('formkey', formkey());
form.append('comment_id', cid);
form.append('thing', thing);
const xhr = new XMLHttpRequest();
xhr.open("post", `/${type}/${cid}`);
xhr.setRequestHeader('xhr', 'xhr');
xhr.onload=function(){
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["response"]) {
const element = document.getElementById(`${type}-${cid}`);
element.innerHTML = data["response"]
}
else {
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
else document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
for (const btn of btns)
{
btn.disabled = false;
btn.classList.remove('disabled');
}
}, 2000);
}
xhr.send(form)
}

View file

@ -1,158 +1,158 @@
const fav = document.getElementById('EMOJIS_favorite')
const search_bar = document.getElementById('emoji_search')
const search_container = document.getElementById('emoji-tab-search')
let emojis = 1
function loadEmojis(form) {
if (emojis == 1)
{
const xhr = new XMLHttpRequest();
xhr.open("GET", '/marsey_list');
xhr.setRequestHeader('xhr', 'xhr');
var f = new FormData();
xhr.onload = function() {
emojis = {
'marsey': JSON.parse(xhr.response),
'marseyalphabet': ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','exclamationpoint','space','period','questionmark'],
'platy': ['platytrans','platyfuckyou','plarsy','platyabused','platyblizzard','platyboxer','platydevil','platyfear','platygirlmagic','platygolong','platyhaes','platyking','platylove','platyneet','platyold','platypatience','platypopcorn','platyrich','platysarcasm','platysilly','platysleeping','platythink','platytired','platytuxedomask','platyblush','platybruh','platycaveman','platycheer','platydown','platyeyes','platyheart','platylol','platymicdrop','platynooo','platysalute','platyseethe','platythumbsup','platywave'],
'tay': ['taylove','tayaaa','tayadmire','taycat','taycelebrate','taychefkiss','taychristmas','tayclap','taycold','taycrown','tayflex','tayflirt','taygrimacing','tayhappy','tayheart','tayhmm','tayhuh','tayhyperdab','tayjammin','taylaugh','taymindblown','tayno','taynod','taypeace','taypray','tayrun','tayscrunch','tayshake','tayshrug','taysilly','tayslide','taysmart','taystop','taytantrum','taytea','taythink','tayvibin','taywhat','taywine','taywine2','taywink','tayyes'],
'classic': ['flowers','scootlgbt','idhitit','2thumbsup','aliendj','ambulance','angry','angrywhip','argue','aroused','ashamed','badass','banana','band','banghead','batman','bigeyes','bite','blind','blowkiss','blush','bong','bounce','bow','breakheart','bs','cartwheel','cat','celebrate','chainsaw','cheers','clap','cold','confused','crazyeyes','cry','cthulhu','cute','laughing','daydream','ddr','deadpool','devilsmile','diddle','die','distress','disturbing','dizzy','domo','doughboy','drink','drool','dudeweedlmao','edward','electro','elephant','embarrassed','emo','emo2','evil','evilclown','evilgrin','facepalm','fap','flamethrower','flipbird','flirt','frown','gasp','glomp','go','gooby','grr','gtfo','guitar','haha','handshake','happydance','headbang','heart','heartbeat','hearts','highfive','hmm','hmph','holdhands','horny','hug','hugging','hugs','hump','humpbed','hysterical','ily','inlove','jason','jawdrop','jedi','jester','kaboom','kick','kiss','kitty','laughchair','lick','link','lol','lolbeat','loving','makeout','medal','megaman','megamanguitar','meow','metime','mooning','mummy','na','nauseous','nervous','ninja','nod','nono','omg','onfire','ooo','orly','tongueout','paddle','panda','pandabutt','paranoid','party','pat','peek','pikachu','pimp','plzdie','poke','popcorn','pout','probe','puke','punch','quote','raccoon','roar','rofl','roflmao','rolleyes','sad','sadeyes','sadhug','samurai','sarcasm','scoot','scream','shmoopy','shrug','skull','slap','slapfight','sleepy','smackfish','smackhead','smh','smile','smoke','sonic','spank','sparta','sperm','spiderman','stab','star','stare','stfu','suicide','surprisehug','suspicious','sweat','swordfight','taco','talk2hand','tantrum','teehee','thinking','threesome','throw','throwaway','tickle','typing','uhuh','vampbat','viking','violin','vulgar','wah','wat','whip','whipping','wink','witch','wizard','woah','worm','woo','work','worship','wow','xd','yay','zzz'],
'rage': ['trolldespair','clueless','troll','bitchplease','spit','challengeaccepted','contentiouscereal','cryingatcuteness','derp','derpcornsyrup','derpcrying','derpcute','derpdumb','derpeuphoria','derpinahd','derpinapokerface','derpinasnickering','derpprocessing','derprealization','derpsnickering','derptalking','derpthinking','derpthumbsup','derpunimpressed','derpwhy','donotwant','epicfacefeatures','fancywithwine','fffffffuuuuuuuuuuuu','flipthetable','foreveralone','foreveralonehappy','hewillnever','idontknow','interuptedreading','iseewhatyoudidthere','killherkillher','ledesire','leexcited','legenius','lelolidk','lemiddlefinger','lemindblown','leokay','lepanicrunning','lepokerface','lepokerface2','lerageface','leseriousface','likeaboss','lolface','longwhiskers','manymiddlefingers','megusta','motherfucker','motherofgod','mysides','ohgodwhy','pervertedspiderman','picard','ragestrangle','rukiddingme','tfwyougettrolled','trollolol','truestorybro','xallthey','yuno'],
'wojak': ['purerage','naziseethe','holdupjak','ethottalking','chadwomanasian','chadwomanblack','chadwomanlatinx','chadwomannordic','trumpjaktalking','rdramajanny','soyreddit','doomerboy','npcsupport','npcoppse','grugthink','soyconsoomer','soyjaktalking','soyquack','tradboy','sciencejak','soyjakanimeglasses','soymad','boomerportrait','soycry','punchjak','seethejak','chadyes','chadno','abusivewife','ancap','bardfinn','bloomer','boomer','boomermonster','brainletbush','brainletcaved','brainletchair','brainletchest','brainletmaga','brainletpit','chad','chadarab','chadasian','chadblack','chadjesus','chadjew','chadjihadi','chadlatino','chadlibleft','chadnordic','chadsikh','chadusa','coomer','doomer','doomerfront','doomergirl','ethot','fatbrain','fatpriest','femboy','gogetter','grug','monke','nazijak','npc','npcfront','npcmaga','psychojak','ragejak','ragemask','ramonajak','soyjackwow','soyjak','soyjakfront','soyjakhipster','soyjakmaga','soyjakyell','tomboy','zoomer','zoomersoy'],
'flags': ['russia','niger','lgbt','animesexual','blacknation','blm','blueline','dreamgender','fatpride','incelpride','israel','kazakhstan','landlordlove','scalperpride','superstraight','trans','translord','transracial','usa'],
'wolf': ['wombiezolf','wolfdramanomicon','wolfamogus','wolfmarine','wolfromulusremus','wolfrope','wolftinfoil','wolfmarseymask','wolfputin','wolfdrama','wolfcumjar','wolflgbt','wolfmarseyhead','wolfnoir','wolfsherifssmoking','wolftrans','wolfvaporwave','wolfangry','wolfbrains','wolfcry','wolfdead','wolfdevilish','wolffacepalm','wolfhappy','wolfidea','wolfkoala','wolflaugh','wolflove','wolfmeditate','wolfphone','wolfrainbow','wolfroses','wolfsad','wolfsfear','wolfsleep','wolftear','wolfthink','wolfthumbsup','wolfupsidedown','wolfvictory','wolfwave','wolfwink'],
'misc': ['chadyescapy','chadnocapy','capygitcommit','capysneedboat','chadbasedcapy','chadcopecapy','chaddilatecapy','chaddonekingcapy','chaddonequeencapy','chadfixedkingcapy','chadfixedqueencapy','chadokcapy','chadseethecapy','chadsneedcapy','chadthankskingcapy','chadthanksqueencapy','sherpat','xdoubt','gigachadjesus','yotsubafish','yotsubalol','sigmatalking','zoroarkconfused','zoroarkhappy','zoroarkpout','zoroarksleepy','casanovanova','deuxwaifu','flairlessmong','hardislife','redditgigachad','rfybear','etika','sneed','retardedchildren','bruh','autism','doot','kylieface','queenyes','wholesomeseal','gigachadglow','gigachadorthodox','gigachad','gigachad2','gigachad3']
}
cringetopia = document.getElementById('EMOJIS_cringetopia')
if (cringetopia)
emojis['cringetopia'] = ['19dollarfortnitecard','ahawow','ahayeah','alex','amogus','amogusbubble','amongpoop','amusing','anarchy','angelblob','angry','angryman','awaakesoyjack','backseatmodding','based','bassproshop','berkfail','berkstache','bert','bestmovieever','binface','blackpill','blingbob','blingbobhd','bluepill','booba','boof','boohoo','brainlet','britishmoment','brownnose','bruh','bruhsmoke','brunocheers','brunopog','bussin','byelol','canigetmod','car','cereal','cerkies','chad','chaddiscordmoderator','chadstare','cheeks','cheers','cheesed','cheeseu','chungus','chunky','clueless','cocka','cokeza','comeagain','coom','cope','coping','cryaboutit','cryingcool','cryrage','cum','cummies','cutelittledude','da','dab','dababy','datrolly','davyree','dayum','death','delight','disbelief','disgostang','disgust','donotdisturb','doritoberk','downisrael','downvote','downwert','drbob','drumtime','erotic','evilblob','face','facepalm','factsandlogic','fake','fatcryrage','fear','fedora','fistbumpl','fistbumpr','fjalsunna','floppacheers','floppagun','fluoride','flushspin','funwa','furrymurder','gas','getajob','gigachad','gigajammin','godiwishthatwereme','gogetter','goingcrazy','goinginsane','gold','gone','goofy','goonpog','goteem','grabhand','grind','gun','hahawyd','hatred','heart','heh','hehehe','hesapeonyouknow','hesepsteinyouknow','hesgayyouknow','hesretardedyouknow','hesrightyouknow','hitormiss','holy','hopeless','horny','hot','hulk','hunky','iloveyou','insanity','ipgrabber','irantroll','isawthat','itendsnow','itsover','itsoverseal','jewbob','joebiden','josh','kanyerant','keith','kill','kissin','laughing','lean','leftgun','lessgooooooooooo','lessgoooooooooooo','letsfuckinggooooooooooo','lick','light','like','littledrummerboy','lmao','logo','logoff','lolrage','love','madman','malding','maldinggif','manpain','march','me_lon','menendez','mewhen','mf','mhm','mhmdoubt','mlady','murder','nefarious','nefariousacts','newport','newport~1','nibburger_king','nigerianreaction','nooo','noooo','noose','noperms','normalize','ohwow','okboomer','orange','pain','pardon','payne','peepotalk','pepecringe','pepecringe2','pepeherrington','peperagecry','pepoboner','pepoojamm','phillip','pitchfork','pleading','please','pouroneout40','praiseallah','prayge','psycho','quagmirelet','ratio','real','really','redditnation','redpill','redwojack','regice','ricardosmile','ripbozo','sad','sadcat','sadge','sadtroll','salute','scare','schizothread','secretside','sheesh','shh','shocked','shotty','sigma','sigmamale','sigmasnap','simpjack','siren','skill_issue','slime','slow','smoketime','sniff','society','society2','soy','spit','spit2','splendid','squidwardno','stare','stare1','stfu','stoppedwert','stopwert','susamogus','suscoin','susge','susiety','tacobell','tempest','thanosd','thanoslol','thistbh','thonk','tiktokcoin','tiresome','tm06','troll','trollcrazy','trollcrazyfurry','trollcrazypoint','trollcrazyrussia','trollcrazyukraine','trolldisappointed','trollface','trollinsane','trollsus','truetrue','turbobrainlet','tuvvokmoment','twetwe','unblockme','updonda','upiran','upvote','upwert','vengeance','virgin','wa','waaaah','wallstreet','wertegg','wertpinion','what','whatdeath','whensus','wholesome','wolfcode','womanmoment','woohoo','wtff','xplode','yeeeees','yeet','yes','yeshoney','yet','you','zombie_phillip']
loadEmojis(form);
}
xhr.send(f);
}
else {
fav.setAttribute('data-form-destination', form)
const commentBox = document.getElementById(form)
commentBox.setAttribute('data-curr-pos', commentBox.selectionStart);
if (fav.innerHTML == "")
{
let str = ""
const favorite_emojis = JSON.parse(localStorage.getItem("favorite_emojis"))
if (favorite_emojis)
{
const sortable = Object.fromEntries(
Object.entries(favorite_emojis).sort(([,a],[,b]) => b-a)
);
for (const emoji of Object.keys(sortable).slice(0, 25))
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${emoji}')" data-bs-toggle="tooltip" title=":${emoji}:" delay:="0"><img loading="lazy" width=50 src="/e/${emoji}.webp" alt="${emoji}-emoji"></button>`
fav.innerHTML = str
}
}
if (search_bar.value == "") {
search_container.innerHTML = ""
document.getElementById("tab-content").classList.remove("d-none");
if (document.getElementById("EMOJIS_marsey").innerHTML == "")
{
for (const [k, v] of Object.entries(emojis)) {
let container = document.getElementById(`EMOJIS_${k}`)
let str = ''
if (k == 'marsey')
{
for (const e of v) {
let k = e.toLowerCase().split(" : ")[0];
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${k}')" style="background: None!important; width:60px; overflow: hidden; border: none" data-bs-toggle="tooltip" title=":${k}:" delay:="0"><img loading="lazy" width=50 src="/e/${k}.webp" alt="${k}-emoji"></button>`;
}
}
else {
for (const e of v) {
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${e}')" style="background: None!important; width:60px; overflow: hidden; border: none" data-bs-toggle="tooltip" title=":${e}:" delay:="0"><img loading="lazy" width=50 src="/e/${e}.webp" alt="${e}-emoji"></button>`;
}
}
container.innerHTML = str
}
}
}
else {
let str = ''
for (const [key, value] of Object.entries(emojis)) {
if (key == "marsey")
{
for (const e of value) {
let arr = e.toLowerCase().split(" : ");
let k = arr[0];
let v = arr[1];
if (str.includes(`'${k}'`)) continue;
if (k.match(search_bar.value.toLowerCase()) || search_bar.value.toLowerCase().match(k) || v.match(search_bar.value.toLowerCase())) {
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${k}')" data-bs-toggle="tooltip" title=":${k}:" delay:="0"><img loading="lazy" width=50 src="/e/${k}.webp" alt="${k}-emoji"></button>`;
}
}
}
else if (key != "marseyalphabet")
{
for (const e of value) {
if (e.match(search_bar.value.toLowerCase()) || search_bar.value.toLowerCase().match(e)) {
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${e}')" style="background: None!important; width:60px; overflow: hidden; border: none" data-bs-toggle="tooltip" title=":${e}:" delay:="0"><img loading="lazy" width=50 src="/e/${e}.webp" alt="${e}-emoji"></button>`;
}
}
}
}
document.getElementById("tab-content").classList.add("d-none");
search_container.innerHTML = str
}
search_bar.oninput = function () {
loadEmojis(form);
};
}
}
function getEmoji(searchTerm) {
const form = fav.getAttribute('data-form-destination')
const commentBox = document.getElementById(form)
const old = commentBox.value;
const curPos = parseInt(commentBox.getAttribute('data-curr-pos'));
const firstHalf = old.slice(0, curPos)
const lastHalf = old.slice(curPos)
let emoji = ':' + searchTerm + ':'
const previousChar = firstHalf.slice(-1)
if (firstHalf.length > 0 && previousChar !== " " && previousChar !== "\n") {
emoji = " " + emoji
}
if (lastHalf.length > 0 && lastHalf[0] !== " ") {
emoji = emoji + " "
}
commentBox.value = firstHalf + emoji + lastHalf;
const newPos = curPos + emoji.length
commentBox.setAttribute('data-curr-pos', newPos.toString());
commentBox.dispatchEvent(new Event('input'));
const favorite_emojis = JSON.parse(localStorage.getItem("favorite_emojis")) || {}
if (favorite_emojis[searchTerm]) favorite_emojis[searchTerm] += 1
else favorite_emojis[searchTerm] = 1
localStorage.setItem("favorite_emojis", JSON.stringify(favorite_emojis))
const fav = document.getElementById('EMOJIS_favorite')
const search_bar = document.getElementById('emoji_search')
const search_container = document.getElementById('emoji-tab-search')
let emojis = 1
function loadEmojis(form) {
if (emojis == 1)
{
const xhr = new XMLHttpRequest();
xhr.open("GET", '/marsey_list');
xhr.setRequestHeader('xhr', 'xhr');
var f = new FormData();
xhr.onload = function() {
emojis = {
'marsey': JSON.parse(xhr.response),
'marseyalphabet': ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','exclamationpoint','space','period','questionmark'],
'platy': ['platytrans','platyfuckyou','plarsy','platyabused','platyblizzard','platyboxer','platydevil','platyfear','platygirlmagic','platygolong','platyhaes','platyking','platylove','platyneet','platyold','platypatience','platypopcorn','platyrich','platysarcasm','platysilly','platysleeping','platythink','platytired','platytuxedomask','platyblush','platybruh','platycaveman','platycheer','platydown','platyeyes','platyheart','platylol','platymicdrop','platynooo','platysalute','platyseethe','platythumbsup','platywave'],
'tay': ['taylove','tayaaa','tayadmire','taycat','taycelebrate','taychefkiss','taychristmas','tayclap','taycold','taycrown','tayflex','tayflirt','taygrimacing','tayhappy','tayheart','tayhmm','tayhuh','tayhyperdab','tayjammin','taylaugh','taymindblown','tayno','taynod','taypeace','taypray','tayrun','tayscrunch','tayshake','tayshrug','taysilly','tayslide','taysmart','taystop','taytantrum','taytea','taythink','tayvibin','taywhat','taywine','taywine2','taywink','tayyes'],
'classic': ['flowers','scootlgbt','idhitit','2thumbsup','aliendj','ambulance','angry','angrywhip','argue','aroused','ashamed','badass','banana','band','banghead','batman','bigeyes','bite','blind','blowkiss','blush','bong','bounce','bow','breakheart','bs','cartwheel','cat','celebrate','chainsaw','cheers','clap','cold','confused','crazyeyes','cry','cthulhu','cute','laughing','daydream','ddr','deadpool','devilsmile','diddle','die','distress','disturbing','dizzy','domo','doughboy','drink','drool','dudeweedlmao','edward','electro','elephant','embarrassed','emo','emo2','evil','evilclown','evilgrin','facepalm','fap','flamethrower','flipbird','flirt','frown','gasp','glomp','go','gooby','grr','gtfo','guitar','haha','handshake','happydance','headbang','heart','heartbeat','hearts','highfive','hmm','hmph','holdhands','horny','hug','hugging','hugs','hump','humpbed','hysterical','ily','inlove','jason','jawdrop','jedi','jester','kaboom','kick','kiss','kitty','laughchair','lick','link','lol','lolbeat','loving','makeout','medal','megaman','megamanguitar','meow','metime','mooning','mummy','na','nauseous','nervous','ninja','nod','nono','omg','onfire','ooo','orly','tongueout','paddle','panda','pandabutt','paranoid','party','pat','peek','pikachu','pimp','plzdie','poke','popcorn','pout','probe','puke','punch','quote','raccoon','roar','rofl','roflmao','rolleyes','sad','sadeyes','sadhug','samurai','sarcasm','scoot','scream','shmoopy','shrug','skull','slap','slapfight','sleepy','smackfish','smackhead','smh','smile','smoke','sonic','spank','sparta','sperm','spiderman','stab','star','stare','stfu','suicide','surprisehug','suspicious','sweat','swordfight','taco','talk2hand','tantrum','teehee','thinking','threesome','throw','throwaway','tickle','typing','uhuh','vampbat','viking','violin','vulgar','wah','wat','whip','whipping','wink','witch','wizard','woah','worm','woo','work','worship','wow','xd','yay','zzz'],
'rage': ['trolldespair','clueless','troll','bitchplease','spit','challengeaccepted','contentiouscereal','cryingatcuteness','derp','derpcornsyrup','derpcrying','derpcute','derpdumb','derpeuphoria','derpinahd','derpinapokerface','derpinasnickering','derpprocessing','derprealization','derpsnickering','derptalking','derpthinking','derpthumbsup','derpunimpressed','derpwhy','donotwant','epicfacefeatures','fancywithwine','fffffffuuuuuuuuuuuu','flipthetable','foreveralone','foreveralonehappy','hewillnever','idontknow','interuptedreading','iseewhatyoudidthere','killherkillher','ledesire','leexcited','legenius','lelolidk','lemiddlefinger','lemindblown','leokay','lepanicrunning','lepokerface','lepokerface2','lerageface','leseriousface','likeaboss','lolface','longwhiskers','manymiddlefingers','megusta','motherfucker','motherofgod','mysides','ohgodwhy','pervertedspiderman','picard','ragestrangle','rukiddingme','tfwyougettrolled','trollolol','truestorybro','xallthey','yuno'],
'wojak': ['purerage','naziseethe','holdupjak','ethottalking','chadwomanasian','chadwomanblack','chadwomanlatinx','chadwomannordic','trumpjaktalking','rdramajanny','soyreddit','doomerboy','npcsupport','npcoppse','grugthink','soyconsoomer','soyjaktalking','soyquack','tradboy','sciencejak','soyjakanimeglasses','soymad','boomerportrait','soycry','punchjak','seethejak','chadyes','chadno','abusivewife','ancap','bardfinn','bloomer','boomer','boomermonster','brainletbush','brainletcaved','brainletchair','brainletchest','brainletmaga','brainletpit','chad','chadarab','chadasian','chadblack','chadjesus','chadjew','chadjihadi','chadlatino','chadlibleft','chadnordic','chadsikh','chadusa','coomer','doomer','doomerfront','doomergirl','ethot','fatbrain','fatpriest','femboy','gogetter','grug','monke','nazijak','npc','npcfront','npcmaga','psychojak','ragejak','ragemask','ramonajak','soyjackwow','soyjak','soyjakfront','soyjakhipster','soyjakmaga','soyjakyell','tomboy','zoomer','zoomersoy'],
'flags': ['russia','niger','lgbt','animesexual','blacknation','blm','blueline','dreamgender','fatpride','incelpride','israel','kazakhstan','landlordlove','scalperpride','superstraight','trans','translord','transracial','usa'],
'wolf': ['wombiezolf','wolfdramanomicon','wolfamogus','wolfmarine','wolfromulusremus','wolfrope','wolftinfoil','wolfmarseymask','wolfputin','wolfdrama','wolfcumjar','wolflgbt','wolfmarseyhead','wolfnoir','wolfsherifssmoking','wolftrans','wolfvaporwave','wolfangry','wolfbrains','wolfcry','wolfdead','wolfdevilish','wolffacepalm','wolfhappy','wolfidea','wolfkoala','wolflaugh','wolflove','wolfmeditate','wolfphone','wolfrainbow','wolfroses','wolfsad','wolfsfear','wolfsleep','wolftear','wolfthink','wolfthumbsup','wolfupsidedown','wolfvictory','wolfwave','wolfwink'],
'misc': ['chadyescapy','chadnocapy','capygitcommit','capysneedboat','chadbasedcapy','chadcopecapy','chaddilatecapy','chaddonekingcapy','chaddonequeencapy','chadfixedkingcapy','chadfixedqueencapy','chadokcapy','chadseethecapy','chadsneedcapy','chadthankskingcapy','chadthanksqueencapy','sherpat','xdoubt','gigachadjesus','yotsubafish','yotsubalol','sigmatalking','zoroarkconfused','zoroarkhappy','zoroarkpout','zoroarksleepy','casanovanova','deuxwaifu','flairlessmong','hardislife','redditgigachad','rfybear','etika','sneed','retardedchildren','bruh','autism','doot','kylieface','queenyes','wholesomeseal','gigachadglow','gigachadorthodox','gigachad','gigachad2','gigachad3']
}
cringetopia = document.getElementById('EMOJIS_cringetopia')
if (cringetopia)
emojis['cringetopia'] = ['19dollarfortnitecard','ahawow','ahayeah','alex','amogus','amogusbubble','amongpoop','amusing','anarchy','angelblob','angry','angryman','awaakesoyjack','backseatmodding','based','bassproshop','berkfail','berkstache','bert','bestmovieever','binface','blackpill','blingbob','blingbobhd','bluepill','booba','boof','boohoo','brainlet','britishmoment','brownnose','bruh','bruhsmoke','brunocheers','brunopog','bussin','byelol','canigetmod','car','cereal','cerkies','chad','chaddiscordmoderator','chadstare','cheeks','cheers','cheesed','cheeseu','chungus','chunky','clueless','cocka','cokeza','comeagain','coom','cope','coping','cryaboutit','cryingcool','cryrage','cum','cummies','cutelittledude','da','dab','dababy','datrolly','davyree','dayum','death','delight','disbelief','disgostang','disgust','donotdisturb','doritoberk','downisrael','downvote','downwert','drbob','drumtime','erotic','evilblob','face','facepalm','factsandlogic','fake','fatcryrage','fear','fedora','fistbumpl','fistbumpr','fjalsunna','floppacheers','floppagun','fluoride','flushspin','funwa','furrymurder','gas','getajob','gigachad','gigajammin','godiwishthatwereme','gogetter','goingcrazy','goinginsane','gold','gone','goofy','goonpog','goteem','grabhand','grind','gun','hahawyd','hatred','heart','heh','hehehe','hesapeonyouknow','hesepsteinyouknow','hesgayyouknow','hesretardedyouknow','hesrightyouknow','hitormiss','holy','hopeless','horny','hot','hulk','hunky','iloveyou','insanity','ipgrabber','irantroll','isawthat','itendsnow','itsover','itsoverseal','jewbob','joebiden','josh','kanyerant','keith','kill','kissin','laughing','lean','leftgun','lessgooooooooooo','lessgoooooooooooo','letsfuckinggooooooooooo','lick','light','like','littledrummerboy','lmao','logo','logoff','lolrage','love','madman','malding','maldinggif','manpain','march','me_lon','menendez','mewhen','mf','mhm','mhmdoubt','mlady','murder','nefarious','nefariousacts','newport','newport~1','nibburger_king','nigerianreaction','nooo','noooo','noose','noperms','normalize','ohwow','okboomer','orange','pain','pardon','payne','peepotalk','pepecringe','pepecringe2','pepeherrington','peperagecry','pepoboner','pepoojamm','phillip','pitchfork','pleading','please','pouroneout40','praiseallah','prayge','psycho','quagmirelet','ratio','real','really','redditnation','redpill','redwojack','regice','ricardosmile','ripbozo','sad','sadcat','sadge','sadtroll','salute','scare','schizothread','secretside','sheesh','shh','shocked','shotty','sigma','sigmamale','sigmasnap','simpjack','siren','skill_issue','slime','slow','smoketime','sniff','society','society2','soy','spit','spit2','splendid','squidwardno','stare','stare1','stfu','stoppedwert','stopwert','susamogus','suscoin','susge','susiety','tacobell','tempest','thanosd','thanoslol','thistbh','thonk','tiktokcoin','tiresome','tm06','troll','trollcrazy','trollcrazyfurry','trollcrazypoint','trollcrazyrussia','trollcrazyukraine','trolldisappointed','trollface','trollinsane','trollsus','truetrue','turbobrainlet','tuvvokmoment','twetwe','unblockme','updonda','upiran','upvote','upwert','vengeance','virgin','wa','waaaah','wallstreet','wertegg','wertpinion','what','whatdeath','whensus','wholesome','wolfcode','womanmoment','woohoo','wtff','xplode','yeeeees','yeet','yes','yeshoney','yet','you','zombie_phillip']
loadEmojis(form);
}
xhr.send(f);
}
else {
fav.setAttribute('data-form-destination', form)
const commentBox = document.getElementById(form)
commentBox.setAttribute('data-curr-pos', commentBox.selectionStart);
if (fav.innerHTML == "")
{
let str = ""
const favorite_emojis = JSON.parse(localStorage.getItem("favorite_emojis"))
if (favorite_emojis)
{
const sortable = Object.fromEntries(
Object.entries(favorite_emojis).sort(([,a],[,b]) => b-a)
);
for (const emoji of Object.keys(sortable).slice(0, 25))
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${emoji}')" data-bs-toggle="tooltip" title=":${emoji}:" delay:="0"><img loading="lazy" width=50 src="/e/${emoji}.webp" alt="${emoji}-emoji"></button>`
fav.innerHTML = str
}
}
if (search_bar.value == "") {
search_container.innerHTML = ""
document.getElementById("tab-content").classList.remove("d-none");
if (document.getElementById("EMOJIS_marsey").innerHTML == "")
{
for (const [k, v] of Object.entries(emojis)) {
let container = document.getElementById(`EMOJIS_${k}`)
let str = ''
if (k == 'marsey')
{
for (const e of v) {
let k = e.toLowerCase().split(" : ")[0];
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${k}')" style="background: None!important; width:60px; overflow: hidden; border: none" data-bs-toggle="tooltip" title=":${k}:" delay:="0"><img loading="lazy" width=50 src="/e/${k}.webp" alt="${k}-emoji"></button>`;
}
}
else {
for (const e of v) {
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${e}')" style="background: None!important; width:60px; overflow: hidden; border: none" data-bs-toggle="tooltip" title=":${e}:" delay:="0"><img loading="lazy" width=50 src="/e/${e}.webp" alt="${e}-emoji"></button>`;
}
}
container.innerHTML = str
}
}
}
else {
let str = ''
for (const [key, value] of Object.entries(emojis)) {
if (key == "marsey")
{
for (const e of value) {
let arr = e.toLowerCase().split(" : ");
let k = arr[0];
let v = arr[1];
if (str.includes(`'${k}'`)) continue;
if (k.match(search_bar.value.toLowerCase()) || search_bar.value.toLowerCase().match(k) || v.match(search_bar.value.toLowerCase())) {
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${k}')" data-bs-toggle="tooltip" title=":${k}:" delay:="0"><img loading="lazy" width=50 src="/e/${k}.webp" alt="${k}-emoji"></button>`;
}
}
}
else if (key != "marseyalphabet")
{
for (const e of value) {
if (e.match(search_bar.value.toLowerCase()) || search_bar.value.toLowerCase().match(e)) {
str += `<button class="btn m-1 px-0 emoji2" onclick="getEmoji('${e}')" style="background: None!important; width:60px; overflow: hidden; border: none" data-bs-toggle="tooltip" title=":${e}:" delay:="0"><img loading="lazy" width=50 src="/e/${e}.webp" alt="${e}-emoji"></button>`;
}
}
}
}
document.getElementById("tab-content").classList.add("d-none");
search_container.innerHTML = str
}
search_bar.oninput = function () {
loadEmojis(form);
};
}
}
function getEmoji(searchTerm) {
const form = fav.getAttribute('data-form-destination')
const commentBox = document.getElementById(form)
const old = commentBox.value;
const curPos = parseInt(commentBox.getAttribute('data-curr-pos'));
const firstHalf = old.slice(0, curPos)
const lastHalf = old.slice(curPos)
let emoji = ':' + searchTerm + ':'
const previousChar = firstHalf.slice(-1)
if (firstHalf.length > 0 && previousChar !== " " && previousChar !== "\n") {
emoji = " " + emoji
}
if (lastHalf.length > 0 && lastHalf[0] !== " ") {
emoji = emoji + " "
}
commentBox.value = firstHalf + emoji + lastHalf;
const newPos = curPos + emoji.length
commentBox.setAttribute('data-curr-pos', newPos.toString());
commentBox.dispatchEvent(new Event('input'));
const favorite_emojis = JSON.parse(localStorage.getItem("favorite_emojis")) || {}
if (favorite_emojis[searchTerm]) favorite_emojis[searchTerm] += 1
else favorite_emojis[searchTerm] = 1
localStorage.setItem("favorite_emojis", JSON.stringify(favorite_emojis))
}

View file

@ -1,7 +1,7 @@
new BugController({
imageSprite: "/assets/images/fireflies.webp",
canDie: false,
minBugs: 10,
maxBugs: 30,
mouseOver: "multiply"
new BugController({
imageSprite: "/assets/images/fireflies.webp",
canDie: false,
minBugs: 10,
maxBugs: 30,
mouseOver: "multiply"
});

File diff suppressed because one or more lines are too long

View file

@ -1,124 +1,124 @@
function autoExpand (field) {
xpos=window.scrollX;
ypos=window.scrollY;
field.style.height = 'inherit';
var computed = window.getComputedStyle(field);
var height = parseInt(computed.getPropertyValue('border-top-width'), 10)
+ parseInt(computed.getPropertyValue('padding-top'), 10)
+ field.scrollHeight
+ parseInt(computed.getPropertyValue('padding-bottom'), 10)
+ parseInt(computed.getPropertyValue('border-bottom-width'), 10);
field.style.height = height + 'px';
window.scrollTo(xpos,ypos);
};
document.addEventListener('input', function (event) {
if (event.target.tagName.toLowerCase() !== 'textarea') return;
autoExpand(event.target);
}, false);
function formkey() {
let formkey = document.getElementById("formkey")
if (formkey) return formkey.innerHTML;
else return null;
}
function bs_trigger(e) {
const images = e.querySelectorAll('img[alt^="![]("]');
for (const e of images) {
e.setAttribute("data-bs-toggle", "modal")
e.setAttribute("data-bs-target", "#expandImageModal")
e.onclick = function(e) {
const image = e.target.src
document.getElementById("desktop-expanded-image").src = image.replace("200w_d.webp", "giphy.webp");
document.getElementById("desktop-expanded-image-link").href = image;
document.getElementById("desktop-expanded-image-wrap-link").href = image;
}
}
let tooltipTriggerList = [].slice.call(e.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function(element){
return bootstrap.Tooltip.getOrCreateInstance(element);
});
const popoverTriggerList = [].slice.call(e.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function(popoverTriggerEl) {
const popoverId = popoverTriggerEl.getAttribute('data-content-id');
let contentEl;
try {contentEl = e.getElementById(popoverId);}
catch(t) {contentEl = document.getElementById(popoverId);}
if (contentEl) {
return bootstrap.Popover.getOrCreateInstance(popoverTriggerEl, {
content: contentEl.innerHTML,
html: true,
});
}
})
}
bs_trigger(document)
function expandDesktopImage(image) {
document.getElementById("desktop-expanded-image").src = image.replace("200w_d.webp", "giphy.webp");
document.getElementById("desktop-expanded-image-link").href = image;
document.getElementById("desktop-expanded-image-wrap-link").href = image;
};
function post_toast(t, url, reload, data) {
t.disabled = true;
t.classList.add("disabled");
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
if(typeof data === 'object' && data !== null) {
for(let k of Object.keys(data)) {
form.append(k, data[k]);
}
}
xhr.onload = function() {
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (xhr.status >= 200 && xhr.status < 300 && data && data['message']) {
document.getElementById('toast-post-success-text').innerText = data["message"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-success')).show();
if (reload == 1) {location.reload()}
} else {
document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
t.disabled = false;
t.classList.remove("disabled");
}, 2000);
};
xhr.send(form);
}
function escapeHTML(unsafe) {
return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
function changename(s1,s2) {
let files = document.getElementById(s2).files;
let filename = '';
for (const e of files) {
filename += e.name.substr(0, 20) + ', ';
}
document.getElementById(s1).innerHTML = escapeHTML(filename.slice(0, -2));
function autoExpand (field) {
xpos=window.scrollX;
ypos=window.scrollY;
field.style.height = 'inherit';
var computed = window.getComputedStyle(field);
var height = parseInt(computed.getPropertyValue('border-top-width'), 10)
+ parseInt(computed.getPropertyValue('padding-top'), 10)
+ field.scrollHeight
+ parseInt(computed.getPropertyValue('padding-bottom'), 10)
+ parseInt(computed.getPropertyValue('border-bottom-width'), 10);
field.style.height = height + 'px';
window.scrollTo(xpos,ypos);
};
document.addEventListener('input', function (event) {
if (event.target.tagName.toLowerCase() !== 'textarea') return;
autoExpand(event.target);
}, false);
function formkey() {
let formkey = document.getElementById("formkey")
if (formkey) return formkey.innerHTML;
else return null;
}
function bs_trigger(e) {
const images = e.querySelectorAll('img[alt^="![]("]');
for (const e of images) {
e.setAttribute("data-bs-toggle", "modal")
e.setAttribute("data-bs-target", "#expandImageModal")
e.onclick = function(e) {
const image = e.target.src
document.getElementById("desktop-expanded-image").src = image.replace("200w_d.webp", "giphy.webp");
document.getElementById("desktop-expanded-image-link").href = image;
document.getElementById("desktop-expanded-image-wrap-link").href = image;
}
}
let tooltipTriggerList = [].slice.call(e.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function(element){
return bootstrap.Tooltip.getOrCreateInstance(element);
});
const popoverTriggerList = [].slice.call(e.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function(popoverTriggerEl) {
const popoverId = popoverTriggerEl.getAttribute('data-content-id');
let contentEl;
try {contentEl = e.getElementById(popoverId);}
catch(t) {contentEl = document.getElementById(popoverId);}
if (contentEl) {
return bootstrap.Popover.getOrCreateInstance(popoverTriggerEl, {
content: contentEl.innerHTML,
html: true,
});
}
})
}
bs_trigger(document)
function expandDesktopImage(image) {
document.getElementById("desktop-expanded-image").src = image.replace("200w_d.webp", "giphy.webp");
document.getElementById("desktop-expanded-image-link").href = image;
document.getElementById("desktop-expanded-image-wrap-link").href = image;
};
function post_toast(t, url, reload, data) {
t.disabled = true;
t.classList.add("disabled");
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
if(typeof data === 'object' && data !== null) {
for(let k of Object.keys(data)) {
form.append(k, data[k]);
}
}
xhr.onload = function() {
let data
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (xhr.status >= 200 && xhr.status < 300 && data && data['message']) {
document.getElementById('toast-post-success-text').innerText = data["message"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-success')).show();
if (reload == 1) {location.reload()}
} else {
document.getElementById('toast-post-error-text').innerText = "Error, please try again later."
if (data && data["error"]) document.getElementById('toast-post-error-text').innerText = data["error"];
bootstrap.Toast.getOrCreateInstance(document.getElementById('toast-post-error')).show();
}
setTimeout(() => {
t.disabled = false;
t.classList.remove("disabled");
}, 2000);
};
xhr.send(form);
}
function escapeHTML(unsafe) {
return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
function changename(s1,s2) {
let files = document.getElementById(s2).files;
let filename = '';
for (const e of files) {
filename += e.name.substr(0, 20) + ', ';
}
document.getElementById(s1).innerHTML = escapeHTML(filename.slice(0, -2));
}

View file

@ -1,44 +1,44 @@
var prevScrollpos = window.pageYOffset;
window.onscroll = function () {
var currentScrollPos = window.pageYOffset;
var topBar = document.getElementById("fixed-bar-mobile");
var bottomBar = document.getElementById("mobile-bottom-navigation-bar");
var dropdown = document.getElementById("mobileSortDropdown");
var navbar = document.getElementById("navbar");
if (bottomBar != null) {
if (prevScrollpos > currentScrollPos && (window.innerHeight + currentScrollPos) < (document.body.offsetHeight - 65)) {
bottomBar.style.bottom = "0px";
}
else if (currentScrollPos <= 125 && (window.innerHeight + currentScrollPos) < (document.body.offsetHeight - 65)) {
bottomBar.style.bottom = "0px";
}
else if (prevScrollpos > currentScrollPos && (window.innerHeight + currentScrollPos) >= (document.body.offsetHeight - 65)) {
bottomBar.style.bottom = "-50px";
}
else {
bottomBar.style.bottom = "-50px";
}
}
if (topBar != null && dropdown != null) {
if (prevScrollpos > currentScrollPos) {
topBar.style.top = "48px";
navbar.classList.remove("shadow");
}
else if (currentScrollPos <= 125) {
topBar.style.top = "48px";
navbar.classList.remove("shadow");
}
else {
topBar.style.top = "-48px";
dropdown.classList.remove('show');
navbar.classList.add("shadow");
}
}
prevScrollpos = currentScrollPos;
var prevScrollpos = window.pageYOffset;
window.onscroll = function () {
var currentScrollPos = window.pageYOffset;
var topBar = document.getElementById("fixed-bar-mobile");
var bottomBar = document.getElementById("mobile-bottom-navigation-bar");
var dropdown = document.getElementById("mobileSortDropdown");
var navbar = document.getElementById("navbar");
if (bottomBar != null) {
if (prevScrollpos > currentScrollPos && (window.innerHeight + currentScrollPos) < (document.body.offsetHeight - 65)) {
bottomBar.style.bottom = "0px";
}
else if (currentScrollPos <= 125 && (window.innerHeight + currentScrollPos) < (document.body.offsetHeight - 65)) {
bottomBar.style.bottom = "0px";
}
else if (prevScrollpos > currentScrollPos && (window.innerHeight + currentScrollPos) >= (document.body.offsetHeight - 65)) {
bottomBar.style.bottom = "-50px";
}
else {
bottomBar.style.bottom = "-50px";
}
}
if (topBar != null && dropdown != null) {
if (prevScrollpos > currentScrollPos) {
topBar.style.top = "48px";
navbar.classList.remove("shadow");
}
else if (currentScrollPos <= 125) {
topBar.style.top = "48px";
navbar.classList.remove("shadow");
}
else {
topBar.style.top = "-48px";
dropdown.classList.remove('show');
navbar.classList.add("shadow");
}
}
prevScrollpos = currentScrollPos;
}

View file

@ -1,41 +1,41 @@
if (typeof showNewCommentCounts === 'undefined') {
function showNewCommentCounts(postId, newTotal) {
const comments = JSON.parse(localStorage.getItem("comment-counts")) || {}
const lastCount = comments[postId]
if (lastCount) {
const newComments = newTotal - lastCount.c
if (newComments > 0) {
elems = document.getElementsByClassName(`${postId}-new-comments`)
for (const elem of elems)
{
elem.textContent = ` (+${newComments})`
elem.classList.remove("d-none")
}
}
}
}
const LAST_CACHE_CLEAN_ID = "last-cache-clean"
const EXPIRE_INTERVAL_MILLIS = 5 * 24 * 60 * 60 * 1000
const CACHE_CLEAN_INTERVAL = 60 * 60 * 1000
function cleanCache() {
const lastCacheClean = JSON.parse(localStorage.getItem(LAST_CACHE_CLEAN_ID)) || Date.now()
const now = Date.now()
if (now - lastCacheClean > CACHE_CLEAN_INTERVAL) {
const comments = JSON.parse(localStorage.getItem("comment-counts")) || {}
for (let [key, value] of Object.entries(comments)) {
if (now - value.t > EXPIRE_INTERVAL_MILLIS) {
delete comments[key]
}
}
localStorage.setItem("comment-counts", JSON.stringify(comments))
}
localStorage.setItem(LAST_CACHE_CLEAN_ID, JSON.stringify(now))
}
setTimeout(cleanCache, 500)
if (typeof showNewCommentCounts === 'undefined') {
function showNewCommentCounts(postId, newTotal) {
const comments = JSON.parse(localStorage.getItem("comment-counts")) || {}
const lastCount = comments[postId]
if (lastCount) {
const newComments = newTotal - lastCount.c
if (newComments > 0) {
elems = document.getElementsByClassName(`${postId}-new-comments`)
for (const elem of elems)
{
elem.textContent = ` (+${newComments})`
elem.classList.remove("d-none")
}
}
}
}
const LAST_CACHE_CLEAN_ID = "last-cache-clean"
const EXPIRE_INTERVAL_MILLIS = 5 * 24 * 60 * 60 * 1000
const CACHE_CLEAN_INTERVAL = 60 * 60 * 1000
function cleanCache() {
const lastCacheClean = JSON.parse(localStorage.getItem(LAST_CACHE_CLEAN_ID)) || Date.now()
const now = Date.now()
if (now - lastCacheClean > CACHE_CLEAN_INTERVAL) {
const comments = JSON.parse(localStorage.getItem("comment-counts")) || {}
for (let [key, value] of Object.entries(comments)) {
if (now - value.t > EXPIRE_INTERVAL_MILLIS) {
delete comments[key]
}
}
localStorage.setItem("comment-counts", JSON.stringify(comments))
}
localStorage.setItem(LAST_CACHE_CLEAN_ID, JSON.stringify(now))
}
setTimeout(cleanCache, 500)
}

View file

@ -1,35 +1,35 @@
function report_postModal(id) {
submitbutton=document.getElementById("reportPostButton");
document.getElementById("reportPostFormBefore").classList.remove('d-none');
document.getElementById("reportPostFormAfter").classList.add('d-none');
submitbutton.disabled = false;
submitbutton.classList.remove('disabled');
submitbutton.innerHTML='Report post';
reason = document.getElementById("reason")
reason.value = ""
submitbutton.onclick = function() {
this.innerHTML='Reporting post';
this.disabled = true;
this.classList.add('disabled');
const xhr = new XMLHttpRequest();
xhr.open("POST", '/report/post/'+id);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
form.append("reason", reason.value);
xhr.onload=function() {
document.getElementById("reportPostFormBefore").classList.add('d-none');
document.getElementById("reportPostFormAfter").classList.remove('d-none');
};
xhr.onerror=function(){alert(errortext)};
xhr.send(form);
}
function report_postModal(id) {
submitbutton=document.getElementById("reportPostButton");
document.getElementById("reportPostFormBefore").classList.remove('d-none');
document.getElementById("reportPostFormAfter").classList.add('d-none');
submitbutton.disabled = false;
submitbutton.classList.remove('disabled');
submitbutton.innerHTML='Report post';
reason = document.getElementById("reason")
reason.value = ""
submitbutton.onclick = function() {
this.innerHTML='Reporting post';
this.disabled = true;
this.classList.add('disabled');
const xhr = new XMLHttpRequest();
xhr.open("POST", '/report/post/'+id);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
form.append("reason", reason.value);
xhr.onload=function() {
document.getElementById("reportPostFormBefore").classList.add('d-none');
document.getElementById("reportPostFormAfter").classList.remove('d-none');
};
xhr.onerror=function(){alert(errortext)};
xhr.send(form);
}
};

View file

@ -1,59 +1,59 @@
function post(url) {
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
xhr.onload = function() {location.reload();};
xhr.send(form);
};
function updatebgselection(){
var bgselector = document.getElementById("backgroundSelector");
const backgrounds = [
{
folder: "space",
backgrounds:
[
"1.webp",
]
},
]
let bgContainer = document.getElementById(`bgcontainer`);
bgContainer.innerHTML = '';
let bgsToDisplay = backgrounds[bgselector.selectedIndex].backgrounds;
let bgsDir = backgrounds[bgselector.selectedIndex].folder;
for (i=0; i < bgsToDisplay.length; i++) {
let onclickPost = bgsDir + "/" + bgsToDisplay[i];
let img = document.createElement('IMG');
img.className = 'bg-image';
img.src = `/assets/images/backgrounds/${bgsDir}/${bgsToDisplay[i]}?v=3`;
img.alt = `${bgsToDisplay[i]}-background`;
img.addEventListener('click', () => {
post(`/settings/profile?background=${onclickPost}`);
});
let button = document.createElement('BUTTON');
button.className = "btn btn-secondary bg-button";
button.appendChild(img);
bgContainer.appendChild(button);
}
}
updatebgselection();
document.onpaste = function(event) {
var focused = document.activeElement;
if (focused.id == 'bio-text') {
f=document.getElementById('file-upload');
files = event.clipboardData.files
filename = files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
f.files = files;
document.getElementById('filename-show').textContent = filename;
}
}
function post(url) {
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("formkey", formkey());
xhr.onload = function() {location.reload();};
xhr.send(form);
};
function updatebgselection(){
var bgselector = document.getElementById("backgroundSelector");
const backgrounds = [
{
folder: "space",
backgrounds:
[
"1.webp",
]
},
]
let bgContainer = document.getElementById(`bgcontainer`);
bgContainer.innerHTML = '';
let bgsToDisplay = backgrounds[bgselector.selectedIndex].backgrounds;
let bgsDir = backgrounds[bgselector.selectedIndex].folder;
for (i=0; i < bgsToDisplay.length; i++) {
let onclickPost = bgsDir + "/" + bgsToDisplay[i];
let img = document.createElement('IMG');
img.className = 'bg-image';
img.src = `/assets/images/backgrounds/${bgsDir}/${bgsToDisplay[i]}?v=3`;
img.alt = `${bgsToDisplay[i]}-background`;
img.addEventListener('click', () => {
post(`/settings/profile?background=${onclickPost}`);
});
let button = document.createElement('BUTTON');
button.className = "btn btn-secondary bg-button";
button.appendChild(img);
bgContainer.appendChild(button);
}
}
updatebgselection();
document.onpaste = function(event) {
var focused = document.activeElement;
if (focused.id == 'bio-text') {
f=document.getElementById('file-upload');
files = event.clipboardData.files
filename = files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
f.files = files;
document.getElementById('filename-show').textContent = filename;
}
}
}

View file

@ -1,41 +1,41 @@
document.getElementById('password-register').addEventListener('input', function () {
var charCount = document.getElementById("password-register").value;
var id = document.getElementById("passwordHelpRegister");
var successID = document.getElementById("passwordHelpSuccess");
if (charCount.length >= 8) {
id.classList.add("d-none");
successID.classList.remove("d-none");
} else {
id.classList.remove("d-none");
successID.classList.add("d-none");
}
});
document.getElementById('username-register').addEventListener('input', function () {
const userName = document.getElementById("username-register").value;
const id = document.getElementById("usernameHelpRegister");
if (/[^a-zA-Z0-9_\-$]/.test(userName)) {
id.innerHTML = '<span class="form-text font-weight-bold text-danger mt-1">No special characters or spaces allowed.</span>';
} else {
id.innerHTML = '<span class="form-text font-weight-bold text-success mt-1">Username is a-okay!</span>';
if (userName.length < 3) {
id.innerHTML = '<span class="form-text font-weight-bold text-muted mt-1">Username must be at least 3 characters long.</span>';
} else if (userName.length > 25) {
id.innerHTML = '<span class="form-text font-weight-bold text-danger mt-1">Username must be 25 characters or less.</span>';
}
else {
fetch('/is_available/' + userName)
.then(res => res.json())
.then(json => {
if (!json[userName]) {
id.innerHTML = '<span class="form-text font-weight-bold text-danger mt-1">Username already taken :(</span>';
}
})
}
}
document.getElementById('password-register').addEventListener('input', function () {
var charCount = document.getElementById("password-register").value;
var id = document.getElementById("passwordHelpRegister");
var successID = document.getElementById("passwordHelpSuccess");
if (charCount.length >= 8) {
id.classList.add("d-none");
successID.classList.remove("d-none");
} else {
id.classList.remove("d-none");
successID.classList.add("d-none");
}
});
document.getElementById('username-register').addEventListener('input', function () {
const userName = document.getElementById("username-register").value;
const id = document.getElementById("usernameHelpRegister");
if (/[^a-zA-Z0-9_\-$]/.test(userName)) {
id.innerHTML = '<span class="form-text font-weight-bold text-danger mt-1">No special characters or spaces allowed.</span>';
} else {
id.innerHTML = '<span class="form-text font-weight-bold text-success mt-1">Username is a-okay!</span>';
if (userName.length < 3) {
id.innerHTML = '<span class="form-text font-weight-bold text-muted mt-1">Username must be at least 3 characters long.</span>';
} else if (userName.length > 25) {
id.innerHTML = '<span class="form-text font-weight-bold text-danger mt-1">Username must be 25 characters or less.</span>';
}
else {
fetch('/is_available/' + userName)
.then(res => res.json())
.then(json => {
if (!json[userName]) {
id.innerHTML = '<span class="form-text font-weight-bold text-danger mt-1">Username already taken :(</span>';
}
})
}
}
});

View file

@ -1,164 +1,164 @@
document.getElementById('post-title').value = localStorage.getItem("post_title")
document.getElementById('post-text').value = localStorage.getItem("post_text")
document.getElementById('post-url').value = localStorage.getItem("post_url")
function checkForRequired() {
const title = document.getElementById("post-title");
const url = document.getElementById("post-url");
const text = document.getElementById("post-text");
const button = document.getElementById("create_button");
const image = document.getElementById("file-upload");
const image2 = document.getElementById("file-upload-submit");
if (url.value.length > 0 || image.files.length > 0 || image2.files.length > 0) {
text.required = false;
url.required=false;
} else if (text.value.length > 0 || image.files.length > 0 || image2.files.length > 0) {
url.required = false;
} else {
text.required = true;
url.required = true;
}
const isValidTitle = title.checkValidity();
const isValidURL = url.checkValidity();
const isValidText = text.checkValidity();
if (isValidTitle && (isValidURL || image.files.length > 0 || image2.files.length > 0)) {
button.disabled = false;
} else if (isValidTitle && isValidText) {
button.disabled = false;
} else {
button.disabled = true;
}
}
checkForRequired();
function hide_image() {
x=document.getElementById('image-upload-block');
url=document.getElementById('post-url').value;
if (url.length>=1){
x.classList.add('d-none');
}
else {
x.classList.remove('d-none');
}
}
document.onpaste = function(event) {
files = event.clipboardData.files
filename = files[0]
if (filename)
{
filename = filename.name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
if (document.activeElement.id == 'post-text') {
document.getElementById('file-upload-submit').files = files;
document.getElementById('filename-show-submit').textContent = filename;
}
else {
f=document.getElementById('file-upload');
f.files = files;
document.getElementById('filename-show').textContent = filename;
document.getElementById('urlblock').classList.add('d-none');
var fileReader = new FileReader();
fileReader.readAsDataURL(f.files[0]);
fileReader.addEventListener("load", function () {document.getElementById('image-preview').setAttribute('src', this.result);});
document.getElementById('file-upload').setAttribute('required', 'false');
}
document.getElementById('post-url').value = null;
localStorage.setItem("post_url", "")
}
checkForRequired();
}
}
document.getElementById('file-upload').addEventListener('change', function(){
f=document.getElementById('file-upload');
document.getElementById('urlblock').classList.add('d-none');
document.getElementById('filename-show').textContent = document.getElementById('file-upload').files[0].name.substr(0, 20);
filename = f.files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".webp"))
{
var fileReader = new FileReader();
fileReader.readAsDataURL(f.files[0]);
fileReader.addEventListener("load", function () {document.getElementById('image-preview').setAttribute('src', this.result);});
}
checkForRequired();
})
function savetext() {
localStorage.setItem("post_title", document.getElementById('post-title').value)
localStorage.setItem("post_text", document.getElementById('post-text').value)
localStorage.setItem("post_url", document.getElementById('post-url').value)
let sub = document.getElementById('sub')
if (sub) localStorage.setItem("sub", sub.value)
}
function autoSuggestTitle() {
var urlField = document.getElementById("post-url");
var titleField = document.getElementById("post-title");
var isValidURL = urlField.checkValidity();
if (isValidURL && urlField.value.length > 0 && titleField.value === "") {
var x = new XMLHttpRequest();
x.withCredentials=true;
x.onreadystatechange = function() {
if (x.readyState == 4 && x.status == 200 && !titleField.value) {
title=JSON.parse(x.responseText)["title"];
titleField.value=title;
checkForRequired()
}
}
x.open('get','/submit/title?url=' + urlField.value);
x.send(null);
};
};
function draft(t) {
const followers = document.getElementById("followers")
if (t.checked == true) {
followers.checked = false;
followers.disabled = true;
} else {
followers.disabled = false;
}
}
function checkRepost(t) {
const system = document.getElementById('system')
system.innerHTML = `To post an image, use a direct image link such as i.imgur.com`;
const url = t.value
if (url) {
const xhr = new XMLHttpRequest();
xhr.open("post", "/is_repost");
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("url", url);
xhr.onload=function(){
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["permalink"]) {
const permalink = data["permalink"]
if (permalink) {
system.innerHTML = `<span class='text-danger'>This is a repost of <a href=${permalink}>${permalink}</a></span>`;
}
}
}
xhr.send(form)
}
document.getElementById('post-title').value = localStorage.getItem("post_title")
document.getElementById('post-text').value = localStorage.getItem("post_text")
document.getElementById('post-url').value = localStorage.getItem("post_url")
function checkForRequired() {
const title = document.getElementById("post-title");
const url = document.getElementById("post-url");
const text = document.getElementById("post-text");
const button = document.getElementById("create_button");
const image = document.getElementById("file-upload");
const image2 = document.getElementById("file-upload-submit");
if (url.value.length > 0 || image.files.length > 0 || image2.files.length > 0) {
text.required = false;
url.required=false;
} else if (text.value.length > 0 || image.files.length > 0 || image2.files.length > 0) {
url.required = false;
} else {
text.required = true;
url.required = true;
}
const isValidTitle = title.checkValidity();
const isValidURL = url.checkValidity();
const isValidText = text.checkValidity();
if (isValidTitle && (isValidURL || image.files.length > 0 || image2.files.length > 0)) {
button.disabled = false;
} else if (isValidTitle && isValidText) {
button.disabled = false;
} else {
button.disabled = true;
}
}
checkForRequired();
function hide_image() {
x=document.getElementById('image-upload-block');
url=document.getElementById('post-url').value;
if (url.length>=1){
x.classList.add('d-none');
}
else {
x.classList.remove('d-none');
}
}
document.onpaste = function(event) {
files = event.clipboardData.files
filename = files[0]
if (filename)
{
filename = filename.name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".gif"))
{
if (document.activeElement.id == 'post-text') {
document.getElementById('file-upload-submit').files = files;
document.getElementById('filename-show-submit').textContent = filename;
}
else {
f=document.getElementById('file-upload');
f.files = files;
document.getElementById('filename-show').textContent = filename;
document.getElementById('urlblock').classList.add('d-none');
var fileReader = new FileReader();
fileReader.readAsDataURL(f.files[0]);
fileReader.addEventListener("load", function () {document.getElementById('image-preview').setAttribute('src', this.result);});
document.getElementById('file-upload').setAttribute('required', 'false');
}
document.getElementById('post-url').value = null;
localStorage.setItem("post_url", "")
}
checkForRequired();
}
}
document.getElementById('file-upload').addEventListener('change', function(){
f=document.getElementById('file-upload');
document.getElementById('urlblock').classList.add('d-none');
document.getElementById('filename-show').textContent = document.getElementById('file-upload').files[0].name.substr(0, 20);
filename = f.files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".webp") || filename.endsWith(".webp"))
{
var fileReader = new FileReader();
fileReader.readAsDataURL(f.files[0]);
fileReader.addEventListener("load", function () {document.getElementById('image-preview').setAttribute('src', this.result);});
}
checkForRequired();
})
function savetext() {
localStorage.setItem("post_title", document.getElementById('post-title').value)
localStorage.setItem("post_text", document.getElementById('post-text').value)
localStorage.setItem("post_url", document.getElementById('post-url').value)
let sub = document.getElementById('sub')
if (sub) localStorage.setItem("sub", sub.value)
}
function autoSuggestTitle() {
var urlField = document.getElementById("post-url");
var titleField = document.getElementById("post-title");
var isValidURL = urlField.checkValidity();
if (isValidURL && urlField.value.length > 0 && titleField.value === "") {
var x = new XMLHttpRequest();
x.withCredentials=true;
x.onreadystatechange = function() {
if (x.readyState == 4 && x.status == 200 && !titleField.value) {
title=JSON.parse(x.responseText)["title"];
titleField.value=title;
checkForRequired()
}
}
x.open('get','/submit/title?url=' + urlField.value);
x.send(null);
};
};
function draft(t) {
const followers = document.getElementById("followers")
if (t.checked == true) {
followers.checked = false;
followers.disabled = true;
} else {
followers.disabled = false;
}
}
function checkRepost(t) {
const system = document.getElementById('system')
system.innerHTML = `To post an image, use a direct image link such as i.imgur.com`;
const url = t.value
if (url) {
const xhr = new XMLHttpRequest();
xhr.open("post", "/is_repost");
xhr.setRequestHeader('xhr', 'xhr');
var form = new FormData()
form.append("url", url);
xhr.onload=function(){
try {data = JSON.parse(xhr.response)}
catch(e) {console.log(e)}
if (data && data["permalink"]) {
const permalink = data["permalink"]
if (permalink) {
system.innerHTML = `<span class='text-danger'>This is a repost of <a href=${permalink}>${permalink}</a></span>`;
}
}
}
xhr.send(form)
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,53 +1,53 @@
let u_username = document.getElementById('u_username')
if (u_username)
{
u_username = u_username.innerHTML
let audio = new Audio(`/@${u_username}/song`);
audio.loop=true;
function toggle() {
if (audio.paused) audio.play()
else audio.pause()
}
audio.play();
document.getElementById('userpage').addEventListener('click', () => {
if (audio.paused) audio.play();
}, {once : true});
}
else
{
let v_username = document.getElementById('v_username')
if (v_username)
{
v_username = v_username.innerHTML
const paused = localStorage.getItem("paused")
let audio = new Audio(`/@${v_username}/song`);
audio.loop=true;
function toggle() {
if (audio.paused)
{
audio.play()
localStorage.setItem("paused", "")
}
else
{
audio.pause()
localStorage.setItem("paused", "1")
}
}
if (!paused)
{
audio.play();
window.addEventListener('click', () => {
if (audio.paused) audio.play();
}, {once : true});
}
}
let u_username = document.getElementById('u_username')
if (u_username)
{
u_username = u_username.innerHTML
let audio = new Audio(`/@${u_username}/song`);
audio.loop=true;
function toggle() {
if (audio.paused) audio.play()
else audio.pause()
}
audio.play();
document.getElementById('userpage').addEventListener('click', () => {
if (audio.paused) audio.play();
}, {once : true});
}
else
{
let v_username = document.getElementById('v_username')
if (v_username)
{
v_username = v_username.innerHTML
const paused = localStorage.getItem("paused")
let audio = new Audio(`/@${v_username}/song`);
audio.loop=true;
function toggle() {
if (audio.paused)
{
audio.play()
localStorage.setItem("paused", "")
}
else
{
audio.pause()
localStorage.setItem("paused", "1")
}
}
if (!paused)
{
audio.play();
window.addEventListener('click', () => {
if (audio.paused) audio.play();
}, {once : true});
}
}
}

View file

@ -1,2 +1,2 @@
User-agent: *
User-agent: *
Allow: /

View file

@ -1,21 +1,21 @@
from .alts import *
from .badges import *
from .clients import *
from .comment import *
from .domains import *
from .flags import *
from .user import *
from .userblock import *
from .submission import *
from .votes import *
from .domains import *
from .subscriptions import *
from files.__main__ import app
from .mod_logs import *
from .award import *
from .marsey import *
from .sub_block import *
from .saves import *
from .views import *
from .notifications import *
from .alts import *
from .badges import *
from .clients import *
from .comment import *
from .domains import *
from .flags import *
from .user import *
from .userblock import *
from .submission import *
from .votes import *
from .domains import *
from .subscriptions import *
from files.__main__ import app
from .mod_logs import *
from .award import *
from .marsey import *
from .sub_block import *
from .saves import *
from .views import *
from .notifications import *
from .follows import *

View file

@ -1,14 +1,14 @@
from sqlalchemy import *
from files.__main__ import Base
class Alt(Base):
__tablename__ = "alts"
user1 = Column(Integer, ForeignKey("users.id"), primary_key=True)
user2 = Column(Integer, ForeignKey("users.id"), primary_key=True)
is_manual = Column(Boolean, default=False)
def __repr__(self):
return f"<Alt(id={self.id})>"
from sqlalchemy import *
from files.__main__ import Base
class Alt(Base):
__tablename__ = "alts"
user1 = Column(Integer, ForeignKey("users.id"), primary_key=True)
user2 = Column(Integer, ForeignKey("users.id"), primary_key=True)
is_manual = Column(Boolean, default=False)
def __repr__(self):
return f"<Alt(id={self.id})>"

View file

@ -1,36 +1,36 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from os import environ
from files.helpers.lazy import lazy
from files.helpers.const import *
class AwardRelationship(Base):
__tablename__ = "award_relationships"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
submission_id = Column(Integer, ForeignKey("submissions.id"))
comment_id = Column(Integer, ForeignKey("comments.id"))
kind = Column(String)
user = relationship("User", primaryjoin="AwardRelationship.user_id==User.id", viewonly=True)
post = relationship("Submission", primaryjoin="AwardRelationship.submission_id==Submission.id", viewonly=True)
comment = relationship("Comment", primaryjoin="AwardRelationship.comment_id==Comment.id", viewonly=True)
@property
@lazy
def type(self):
return AWARDS[self.kind]
@property
@lazy
def title(self):
return self.type['title']
@property
@lazy
def class_list(self):
return self.type['icon']+' '+self.type['color']
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from os import environ
from files.helpers.lazy import lazy
from files.helpers.const import *
class AwardRelationship(Base):
__tablename__ = "award_relationships"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
submission_id = Column(Integer, ForeignKey("submissions.id"))
comment_id = Column(Integer, ForeignKey("comments.id"))
kind = Column(String)
user = relationship("User", primaryjoin="AwardRelationship.user_id==User.id", viewonly=True)
post = relationship("Submission", primaryjoin="AwardRelationship.submission_id==Submission.id", viewonly=True)
comment = relationship("Comment", primaryjoin="AwardRelationship.comment_id==Comment.id", viewonly=True)
@property
@lazy
def type(self):
return AWARDS[self.kind]
@property
@lazy
def title(self):
return self.type['title']
@property
@lazy
def class_list(self):
return self.type['icon']+' '+self.type['color']

View file

@ -1,73 +1,73 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base, app
from os import environ
from files.helpers.lazy import lazy
from files.helpers.const import *
from datetime import datetime
from json import loads
class BadgeDef(Base):
__tablename__ = "badge_defs"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String)
description = Column(String)
def __repr__(self):
return f"<BadgeDef(id={self.id})>"
class Badge(Base):
__tablename__ = "badges"
user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
badge_id = Column(Integer, ForeignKey('badge_defs.id'), primary_key=True)
description = Column(String)
url = Column(String)
user = relationship("User", viewonly=True)
badge = relationship("BadgeDef", primaryjoin="foreign(Badge.badge_id) == remote(BadgeDef.id)", viewonly=True)
def __repr__(self):
return f"<Badge(user_id={self.user_id}, badge_id={self.badge_id})>"
@property
@lazy
def text(self):
if self.name == "Chud":
ti = self.user.agendaposter
if ti: text = self.badge.description + " until " + datetime.utcfromtimestamp(ti).strftime('%Y-%m-%d %H:%M:%S')
else: text = self.badge.description + " permanently"
elif self.badge_id in {94,95,96,97,98,109}:
if self.badge_id == 94: ti = self.user.progressivestack
elif self.badge_id == 95: ti = self.user.bird
elif self.badge_id == 96: ti = self.user.flairchanged
elif self.badge_id == 97: ti = self.user.longpost
elif self.badge_id == 98: ti = self.user.marseyawarded
elif self.badge_id == 109: ti = self.user.rehab
text = self.badge.description + " until " + datetime.utcfromtimestamp(ti).strftime('%Y-%m-%d %H:%M:%S')
elif self.description: text = self.description
elif self.badge.description: text = self.badge.description
else: return self.name
return f'{self.name} - {text}'
@property
@lazy
def name(self):
return self.badge.name
@property
@lazy
def path(self):
return f"/assets/images/badges/{self.badge_id}.webp"
@property
@lazy
def json(self):
return {'text': self.text,
'name': self.name,
'url': self.url,
'icon_url':self.path
}
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base, app
from os import environ
from files.helpers.lazy import lazy
from files.helpers.const import *
from datetime import datetime
from json import loads
class BadgeDef(Base):
__tablename__ = "badge_defs"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String)
description = Column(String)
def __repr__(self):
return f"<BadgeDef(id={self.id})>"
class Badge(Base):
__tablename__ = "badges"
user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
badge_id = Column(Integer, ForeignKey('badge_defs.id'), primary_key=True)
description = Column(String)
url = Column(String)
user = relationship("User", viewonly=True)
badge = relationship("BadgeDef", primaryjoin="foreign(Badge.badge_id) == remote(BadgeDef.id)", viewonly=True)
def __repr__(self):
return f"<Badge(user_id={self.user_id}, badge_id={self.badge_id})>"
@property
@lazy
def text(self):
if self.name == "Chud":
ti = self.user.agendaposter
if ti: text = self.badge.description + " until " + datetime.utcfromtimestamp(ti).strftime('%Y-%m-%d %H:%M:%S')
else: text = self.badge.description + " permanently"
elif self.badge_id in {94,95,96,97,98,109}:
if self.badge_id == 94: ti = self.user.progressivestack
elif self.badge_id == 95: ti = self.user.bird
elif self.badge_id == 96: ti = self.user.flairchanged
elif self.badge_id == 97: ti = self.user.longpost
elif self.badge_id == 98: ti = self.user.marseyawarded
elif self.badge_id == 109: ti = self.user.rehab
text = self.badge.description + " until " + datetime.utcfromtimestamp(ti).strftime('%Y-%m-%d %H:%M:%S')
elif self.description: text = self.description
elif self.badge.description: text = self.badge.description
else: return self.name
return f'{self.name} - {text}'
@property
@lazy
def name(self):
return self.badge.name
@property
@lazy
def path(self):
return f"/assets/images/badges/{self.badge_id}.webp"
@property
@lazy
def json(self):
return {'text': self.text,
'name': self.name,
'url': self.url,
'icon_url':self.path
}

View file

@ -1,84 +1,84 @@
from flask import *
from sqlalchemy import *
from sqlalchemy.orm import relationship
from .submission import Submission
from .comment import Comment
from files.__main__ import Base
from files.helpers.lazy import lazy
from files.helpers.const import *
import time
class OauthApp(Base):
__tablename__ = "oauth_apps"
id = Column(Integer, primary_key=True)
client_id = Column(String)
app_name = Column(String)
redirect_uri = Column(String)
description = Column(String)
author_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", viewonly=True)
def __repr__(self): return f"<OauthApp(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
@property
@lazy
def permalink(self): return f"/admin/app/{self.id}"
@lazy
def idlist(self, page=1):
posts = g.db.query(Submission.id).filter_by(app_id=self.id)
posts=posts.order_by(Submission.created_utc.desc())
posts=posts.offset(100*(page-1)).limit(101)
return [x[0] for x in posts.all()]
@lazy
def comments_idlist(self, page=1):
posts = g.db.query(Comment.id).filter_by(app_id=self.id)
posts=posts.order_by(Comment.created_utc.desc())
posts=posts.offset(100*(page-1)).limit(101)
return [x[0] for x in posts.all()]
class ClientAuth(Base):
__tablename__ = "client_auths"
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
oauth_client = Column(Integer, ForeignKey("oauth_apps.id"), primary_key=True)
access_token = Column(String)
user = relationship("User", viewonly=True)
application = relationship("OauthApp", viewonly=True)
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
from flask import *
from sqlalchemy import *
from sqlalchemy.orm import relationship
from .submission import Submission
from .comment import Comment
from files.__main__ import Base
from files.helpers.lazy import lazy
from files.helpers.const import *
import time
class OauthApp(Base):
__tablename__ = "oauth_apps"
id = Column(Integer, primary_key=True)
client_id = Column(String)
app_name = Column(String)
redirect_uri = Column(String)
description = Column(String)
author_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", viewonly=True)
def __repr__(self): return f"<OauthApp(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
@property
@lazy
def permalink(self): return f"/admin/app/{self.id}"
@lazy
def idlist(self, page=1):
posts = g.db.query(Submission.id).filter_by(app_id=self.id)
posts=posts.order_by(Submission.created_utc.desc())
posts=posts.offset(100*(page-1)).limit(101)
return [x[0] for x in posts.all()]
@lazy
def comments_idlist(self, page=1):
posts = g.db.query(Comment.id).filter_by(app_id=self.id)
posts=posts.order_by(Comment.created_utc.desc())
posts=posts.offset(100*(page-1)).limit(101)
return [x[0] for x in posts.all()]
class ClientAuth(Base):
__tablename__ = "client_auths"
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
oauth_client = Column(Integer, ForeignKey("oauth_apps.id"), primary_key=True)
access_token = Column(String)
user = relationship("User", viewonly=True)
application = relationship("OauthApp", viewonly=True)
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))

View file

@ -1,8 +1,8 @@
from sqlalchemy import *
from files.__main__ import Base
class BannedDomain(Base):
__tablename__ = "banneddomains"
domain = Column(String, primary_key=True)
from sqlalchemy import *
from files.__main__ import Base
class BannedDomain(Base):
__tablename__ = "banneddomains"
domain = Column(String, primary_key=True)
reason = Column(String)

View file

@ -1,71 +1,71 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
from files.helpers.const import *
import time
class Flag(Base):
__tablename__ = "flags"
post_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
reason = Column(String)
created_utc = Column(Integer)
user = relationship("User", primaryjoin = "Flag.user_id == User.id", uselist = False, viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Flag(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
@lazy
def realreason(self, v):
return censor_slurs(self.reason, v)
class CommentFlag(Base):
__tablename__ = "commentflags"
comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
reason = Column(String)
created_utc = Column(Integer)
user = relationship("User", primaryjoin = "CommentFlag.user_id == User.id", uselist = False, viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<CommentFlag(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
@lazy
def realreason(self, v):
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
from files.helpers.const import *
import time
class Flag(Base):
__tablename__ = "flags"
post_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
reason = Column(String)
created_utc = Column(Integer)
user = relationship("User", primaryjoin = "Flag.user_id == User.id", uselist = False, viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Flag(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
@lazy
def realreason(self, v):
return censor_slurs(self.reason, v)
class CommentFlag(Base):
__tablename__ = "commentflags"
comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
reason = Column(String)
created_utc = Column(Integer)
user = relationship("User", primaryjoin = "CommentFlag.user_id == User.id", uselist = False, viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<CommentFlag(id={self.id})>"
@property
@lazy
def created_date(self):
return time.strftime("%d %B %Y", time.gmtime(self.created_utc))
@property
@lazy
def created_datetime(self):
return str(time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(self.created_utc)))
@lazy
def realreason(self, v):
return censor_slurs(self.reason, v)

View file

@ -1,424 +1,424 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
import time
from files.helpers.lazy import lazy
from os import environ
from copy import deepcopy
from files.helpers.const import *
class ModAction(Base):
__tablename__ = "modactions"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
kind = Column(String)
target_user_id = Column(Integer, ForeignKey("users.id"))
target_submission_id = Column(Integer, ForeignKey("submissions.id"))
target_comment_id = Column(Integer, ForeignKey("comments.id"))
_note=Column(String)
created_utc = Column(Integer)
user = relationship("User", primaryjoin="User.id==ModAction.user_id", viewonly=True)
target_user = relationship("User", primaryjoin="User.id==ModAction.target_user_id", viewonly=True)
target_post = relationship("Submission", viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<ModAction(id={self.id})>"
@property
@lazy
def age_string(self):
age = int(time.time()) - self.created_utc
if age < 60:
return "just now"
elif age < 3600:
minutes = int(age / 60)
return f"{minutes}m ago"
elif age < 86400:
hours = int(age / 3600)
return f"{hours}hr ago"
elif age < 2678400:
days = int(age / 86400)
return f"{days}d ago"
now = time.gmtime()
ctd = time.gmtime(self.created_utc)
months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year)
if now.tm_mday < ctd.tm_mday:
months -= 1
if months < 12:
return f"{months}mo ago"
else:
years = int(months / 12)
return f"{years}yr ago"
@property
def note(self):
if self.kind=="ban_user":
if self.target_post: return f'for <a href="{self.target_post.permalink}">post</a>'
elif self.target_comment_id: return f'for <a href="/comment/{self.target_comment_id}">comment</a>'
else: return self._note
else:
return self._note or ""
@note.setter
def note(self, x):
self._note=x
@property
@lazy
def string(self):
output = ACTIONTYPES[self.kind]["str"].format(self=self, cc=CC_TITLE)
if self.note: output += f" <i>({self.note})</i>"
return output
@property
@lazy
def target_link(self):
if self.target_user: return f'<a href="{self.target_user.url}">{self.target_user.username}</a>'
elif self.target_post:
if self.target_post.club: return f'<a href="{self.target_post.permalink}">{CC} ONLY</a>'
return f'<a href="{self.target_post.permalink}">{self.target_post.title_html}</a>'
elif self.target_comment_id: return f'<a href="/comment/{self.target_comment_id}?context=8#context">comment</a>'
@property
@lazy
def icon(self):
return ACTIONTYPES[self.kind]['icon']
@property
@lazy
def color(self):
return ACTIONTYPES[self.kind]['color']
@property
@lazy
def permalink(self):
return f"/log/{self.id}"
ACTIONTYPES = {
'agendaposter': {
"str": 'set chud theme on {self.target_link}',
"icon": 'fa-snooze',
"color": 'bg-danger'
},
'approve_app': {
"str": 'approved an application by {self.target_link}',
"icon": 'fa-robot',
"color": 'bg-success'
},
'badge_grant': {
"str": 'granted badge to {self.target_link}',
"icon": 'fa-badge',
"color": 'bg-success'
},
'badge_remove': {
"str": 'removed badge from {self.target_link}',
"icon": 'fa-badge',
"color": 'bg-danger'
},
'ban_comment': {
"str": 'removed {self.target_link}',
"icon": 'fa-comment',
"color": 'bg-danger'
},
'ban_domain': {
"str": 'banned a domain',
"icon": 'fa-globe',
"color": 'bg-danger'
},
'ban_post': {
"str": 'removed post {self.target_link}',
"icon": 'fa-feather-alt',
"color": 'bg-danger'
},
'ban_user': {
"str": 'banned user {self.target_link}',
"icon": 'fa-user-slash',
"color": 'bg-danger'
},
'change_sidebar': {
"str": 'changed the sidebar',
"icon": 'fa-columns',
"color": 'bg-primary'
},
'check': {
"str": 'gave {self.target_link} a checkmark',
"icon": 'fa-badge-check',
"color": 'bg-success'
},
'club_allow': {
"str": 'allowed user {self.target_link} into the {cc}',
"icon": 'fa-golf-club',
"color": 'bg-success'
},
'club_ban': {
"str": 'disallowed user {self.target_link} from the {cc}',
"icon": 'fa-golf-club',
"color": 'bg-danger'
},
'delete_report': {
"str": 'deleted report on {self.target_link}',
"icon": 'fa-flag',
"color": 'bg-danger'
},
'disable_Bots': {
"str": 'disabled Bots',
"icon": 'fa-robot',
"color": 'bg-danger'
},
'disable_Fart mode': {
"str": 'disabled fart mode',
"icon": 'fa-gas-pump-slash',
"color": 'bg-danger'
},
'disable_Read-only mode': {
"str": 'disabled readonly mode',
"icon": 'fa-book',
"color": 'bg-danger'
},
'disable_Signups': {
"str": 'disabled Signups',
"icon": 'fa-users',
"color": 'bg-danger'
},
'disable_under_attack': {
"str": 'disabled under attack mode',
"icon": 'fa-shield',
"color": 'bg-muted'
},
'distinguish_comment': {
"str": 'distinguished {self.target_link}',
"icon": 'fa-crown',
"color": 'bg-success'
},
'distinguish_post': {
"str": 'distinguished {self.target_link}',
"icon": 'fa-crown',
"color": 'bg-success'
},
'distribute': {
"str": 'distributed bet winnings to voters on {self.target_link}',
"icon": 'fa-dollar-sign',
"color": 'bg-success'
},
'dump_cache': {
"str": 'dumped cache',
"icon": 'fa-trash-alt',
"color": 'bg-muted'
},
'edit_post': {
"str": 'edited {self.target_link}',
"icon": 'fa-edit',
"color": 'bg-primary'
},
'enable_Bots': {
"str": 'enabled Bots',
"icon": 'fa-robot',
"color": 'bg-success'
},
'enable_Fart mode': {
"str": 'enabled fart mode',
"icon": 'fa-gas-pump',
"color": 'bg-success'
},
'enable_Read-only mode': {
"str": 'enabled readonly mode',
"icon": 'fa-book',
"color": 'bg-success'
},
'enable_Signups': {
"str": 'enabled Signups',
"icon": 'fa-users',
"color": 'bg-success'
},
'enable_under_attack': {
"str": 'enabled under attack mode',
"icon": 'fa-shield',
"color": 'bg-success'
},
'flair_post': {
"str": 'set a flair on {self.target_link}',
"icon": 'fa-tag',
"color": 'bg-primary'
},
'grant_awards': {
"str": 'granted awards to {self.target_link}',
"icon": 'fa-gift',
"color": 'bg-primary'
},
'link_accounts': {
"str": 'linked {self.target_link}',
"icon": 'fa-link',
"color": 'bg-success'
},
'make_admin': {
"str": 'made {self.target_link} admin',
"icon": 'fa-user-crown',
"color": 'bg-success'
},
'make_meme_admin': {
"str": 'made {self.target_link} meme admin',
"icon": 'fa-user-crown',
"color": 'bg-success'
},
'monthly': {
"str": 'distributed monthly marseybux',
"icon": 'fa-sack-dollar',
"color": 'bg-success'
},
'move_hole': {
"str": 'moved {self.target_link} to <a href="/h/{self.target_post.sub}">/h/{self.target_post.sub}</a>',
"icon": 'fa-manhole',
"color": 'bg-primary'
},
'nuke_user': {
"str": 'removed all content of {self.target_link}',
"icon": 'fa-radiation-alt',
"color": 'bg-danger'
},
'pin_comment': {
"str": 'pinned a {self.target_link}',
"icon": 'fa-thumbtack fa-rotate--45',
"color": 'bg-success'
},
'pin_post': {
"str": 'pinned post {self.target_link}',
"icon": 'fa-thumbtack fa-rotate--45',
"color": 'bg-success'
},
'purge_cache': {
"str": 'purged cache',
"icon": 'fa-memory',
"color": 'bg-muted'
},
'reject_app': {
"str": 'rejected an application request by {self.target_link}',
"icon": 'fa-robot',
"color": 'bg-muted'
},
'remove_admin': {
"str": 'removed {self.target_link} as admin',
"icon": 'fa-user-crown',
"color": 'bg-danger'
},
'remove_meme_admin': {
"str": 'removed {self.target_link} as meme admin',
"icon": 'fa-user-crown',
"color": 'bg-danger'
},
'revert': {
"str": 'reverted {self.target_link} mod actions',
"icon": 'fa-history',
"color": 'bg-danger'
},
'revoke_app': {
"str": 'revoked an application by {self.target_link}',
"icon": 'fa-robot',
"color": 'bg-muted'
},
'set_flair_locked': {
"str": "set {self.target_link}'s flair (locked)",
"icon": 'fa-award',
"color": 'bg-primary'
},
'set_flair_notlocked': {
"str": "set {self.target_link}'s flair (not locked)",
"icon": 'fa-award',
"color": 'bg-primary'
},
'set_nsfw': {
"str": 'set nsfw on post {self.target_link}',
"icon": 'fa-eye-evil',
"color": 'bg-danger'
},
'shadowban': {
"str": 'shadowbanned {self.target_link}',
"icon": 'fa-eye-slash',
"color": 'bg-danger'
},
'unagendaposter': {
"str": 'removed chud theme from {self.target_link}',
"icon": 'fa-snooze',
"color": 'bg-success'
},
'unban_comment': {
"str": 'reinstated {self.target_link}',
"icon": 'fa-comment',
"color": 'bg-success'
},
'unban_domain': {
"str": 'unbanned a domain',
"icon": 'fa-globe',
"color": 'bg-success'
},
'unban_post': {
"str": 'reinstated post {self.target_link}',
"icon": 'fa-feather-alt',
"color": 'bg-success'
},
'unban_user': {
"str": 'unbanned user {self.target_link}',
"icon": 'fa-user',
"color": 'bg-success'
},
'uncheck': {
"str": 'removed checkmark from {self.target_link}',
"icon": 'fa-badge-check',
"color": 'bg-muted'
},
'undistinguish_comment': {
"str": 'un-distinguished {self.target_link}',
"icon": 'fa-crown',
"color": 'bg-muted'
},
'undistinguish_post': {
"str": 'un-distinguished {self.target_link}',
"icon": 'fa-crown',
"color": 'bg-muted'
},
'unnuke_user': {
"str": 'approved all content of {self.target_link}',
"icon": 'fa-radiation-alt',
"color": 'bg-success'
},
'unpin_comment': {
"str": 'un-pinned a {self.target_link}',
"icon": 'fa-thumbtack fa-rotate--45',
"color": 'bg-muted'
},
'unpin_post': {
"str": 'un-pinned post {self.target_link}',
"icon": 'fa-thumbtack fa-rotate--45',
"color": 'bg-muted'
},
'unset_nsfw': {
"str": 'un-set nsfw on post {self.target_link}',
"icon": 'fa-eye-evil',
"color": 'bg-success'
},
'unshadowban': {
"str": 'unshadowbanned {self.target_link}',
"icon": 'fa-eye',
"color": 'bg-success'
}
}
ACTIONTYPES2 = deepcopy(ACTIONTYPES)
ACTIONTYPES2.pop("shadowban")
ACTIONTYPES2.pop("unshadowban")
ACTIONTYPES2.pop("flair_post")
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
import time
from files.helpers.lazy import lazy
from os import environ
from copy import deepcopy
from files.helpers.const import *
class ModAction(Base):
__tablename__ = "modactions"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
kind = Column(String)
target_user_id = Column(Integer, ForeignKey("users.id"))
target_submission_id = Column(Integer, ForeignKey("submissions.id"))
target_comment_id = Column(Integer, ForeignKey("comments.id"))
_note=Column(String)
created_utc = Column(Integer)
user = relationship("User", primaryjoin="User.id==ModAction.user_id", viewonly=True)
target_user = relationship("User", primaryjoin="User.id==ModAction.target_user_id", viewonly=True)
target_post = relationship("Submission", viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<ModAction(id={self.id})>"
@property
@lazy
def age_string(self):
age = int(time.time()) - self.created_utc
if age < 60:
return "just now"
elif age < 3600:
minutes = int(age / 60)
return f"{minutes}m ago"
elif age < 86400:
hours = int(age / 3600)
return f"{hours}hr ago"
elif age < 2678400:
days = int(age / 86400)
return f"{days}d ago"
now = time.gmtime()
ctd = time.gmtime(self.created_utc)
months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year)
if now.tm_mday < ctd.tm_mday:
months -= 1
if months < 12:
return f"{months}mo ago"
else:
years = int(months / 12)
return f"{years}yr ago"
@property
def note(self):
if self.kind=="ban_user":
if self.target_post: return f'for <a href="{self.target_post.permalink}">post</a>'
elif self.target_comment_id: return f'for <a href="/comment/{self.target_comment_id}">comment</a>'
else: return self._note
else:
return self._note or ""
@note.setter
def note(self, x):
self._note=x
@property
@lazy
def string(self):
output = ACTIONTYPES[self.kind]["str"].format(self=self, cc=CC_TITLE)
if self.note: output += f" <i>({self.note})</i>"
return output
@property
@lazy
def target_link(self):
if self.target_user: return f'<a href="{self.target_user.url}">{self.target_user.username}</a>'
elif self.target_post:
if self.target_post.club: return f'<a href="{self.target_post.permalink}">{CC} ONLY</a>'
return f'<a href="{self.target_post.permalink}">{self.target_post.title_html}</a>'
elif self.target_comment_id: return f'<a href="/comment/{self.target_comment_id}?context=8#context">comment</a>'
@property
@lazy
def icon(self):
return ACTIONTYPES[self.kind]['icon']
@property
@lazy
def color(self):
return ACTIONTYPES[self.kind]['color']
@property
@lazy
def permalink(self):
return f"/log/{self.id}"
ACTIONTYPES = {
'agendaposter': {
"str": 'set chud theme on {self.target_link}',
"icon": 'fa-snooze',
"color": 'bg-danger'
},
'approve_app': {
"str": 'approved an application by {self.target_link}',
"icon": 'fa-robot',
"color": 'bg-success'
},
'badge_grant': {
"str": 'granted badge to {self.target_link}',
"icon": 'fa-badge',
"color": 'bg-success'
},
'badge_remove': {
"str": 'removed badge from {self.target_link}',
"icon": 'fa-badge',
"color": 'bg-danger'
},
'ban_comment': {
"str": 'removed {self.target_link}',
"icon": 'fa-comment',
"color": 'bg-danger'
},
'ban_domain': {
"str": 'banned a domain',
"icon": 'fa-globe',
"color": 'bg-danger'
},
'ban_post': {
"str": 'removed post {self.target_link}',
"icon": 'fa-feather-alt',
"color": 'bg-danger'
},
'ban_user': {
"str": 'banned user {self.target_link}',
"icon": 'fa-user-slash',
"color": 'bg-danger'
},
'change_sidebar': {
"str": 'changed the sidebar',
"icon": 'fa-columns',
"color": 'bg-primary'
},
'check': {
"str": 'gave {self.target_link} a checkmark',
"icon": 'fa-badge-check',
"color": 'bg-success'
},
'club_allow': {
"str": 'allowed user {self.target_link} into the {cc}',
"icon": 'fa-golf-club',
"color": 'bg-success'
},
'club_ban': {
"str": 'disallowed user {self.target_link} from the {cc}',
"icon": 'fa-golf-club',
"color": 'bg-danger'
},
'delete_report': {
"str": 'deleted report on {self.target_link}',
"icon": 'fa-flag',
"color": 'bg-danger'
},
'disable_Bots': {
"str": 'disabled Bots',
"icon": 'fa-robot',
"color": 'bg-danger'
},
'disable_Fart mode': {
"str": 'disabled fart mode',
"icon": 'fa-gas-pump-slash',
"color": 'bg-danger'
},
'disable_Read-only mode': {
"str": 'disabled readonly mode',
"icon": 'fa-book',
"color": 'bg-danger'
},
'disable_Signups': {
"str": 'disabled Signups',
"icon": 'fa-users',
"color": 'bg-danger'
},
'disable_under_attack': {
"str": 'disabled under attack mode',
"icon": 'fa-shield',
"color": 'bg-muted'
},
'distinguish_comment': {
"str": 'distinguished {self.target_link}',
"icon": 'fa-crown',
"color": 'bg-success'
},
'distinguish_post': {
"str": 'distinguished {self.target_link}',
"icon": 'fa-crown',
"color": 'bg-success'
},
'distribute': {
"str": 'distributed bet winnings to voters on {self.target_link}',
"icon": 'fa-dollar-sign',
"color": 'bg-success'
},
'dump_cache': {
"str": 'dumped cache',
"icon": 'fa-trash-alt',
"color": 'bg-muted'
},
'edit_post': {
"str": 'edited {self.target_link}',
"icon": 'fa-edit',
"color": 'bg-primary'
},
'enable_Bots': {
"str": 'enabled Bots',
"icon": 'fa-robot',
"color": 'bg-success'
},
'enable_Fart mode': {
"str": 'enabled fart mode',
"icon": 'fa-gas-pump',
"color": 'bg-success'
},
'enable_Read-only mode': {
"str": 'enabled readonly mode',
"icon": 'fa-book',
"color": 'bg-success'
},
'enable_Signups': {
"str": 'enabled Signups',
"icon": 'fa-users',
"color": 'bg-success'
},
'enable_under_attack': {
"str": 'enabled under attack mode',
"icon": 'fa-shield',
"color": 'bg-success'
},
'flair_post': {
"str": 'set a flair on {self.target_link}',
"icon": 'fa-tag',
"color": 'bg-primary'
},
'grant_awards': {
"str": 'granted awards to {self.target_link}',
"icon": 'fa-gift',
"color": 'bg-primary'
},
'link_accounts': {
"str": 'linked {self.target_link}',
"icon": 'fa-link',
"color": 'bg-success'
},
'make_admin': {
"str": 'made {self.target_link} admin',
"icon": 'fa-user-crown',
"color": 'bg-success'
},
'make_meme_admin': {
"str": 'made {self.target_link} meme admin',
"icon": 'fa-user-crown',
"color": 'bg-success'
},
'monthly': {
"str": 'distributed monthly marseybux',
"icon": 'fa-sack-dollar',
"color": 'bg-success'
},
'move_hole': {
"str": 'moved {self.target_link} to <a href="/h/{self.target_post.sub}">/h/{self.target_post.sub}</a>',
"icon": 'fa-manhole',
"color": 'bg-primary'
},
'nuke_user': {
"str": 'removed all content of {self.target_link}',
"icon": 'fa-radiation-alt',
"color": 'bg-danger'
},
'pin_comment': {
"str": 'pinned a {self.target_link}',
"icon": 'fa-thumbtack fa-rotate--45',
"color": 'bg-success'
},
'pin_post': {
"str": 'pinned post {self.target_link}',
"icon": 'fa-thumbtack fa-rotate--45',
"color": 'bg-success'
},
'purge_cache': {
"str": 'purged cache',
"icon": 'fa-memory',
"color": 'bg-muted'
},
'reject_app': {
"str": 'rejected an application request by {self.target_link}',
"icon": 'fa-robot',
"color": 'bg-muted'
},
'remove_admin': {
"str": 'removed {self.target_link} as admin',
"icon": 'fa-user-crown',
"color": 'bg-danger'
},
'remove_meme_admin': {
"str": 'removed {self.target_link} as meme admin',
"icon": 'fa-user-crown',
"color": 'bg-danger'
},
'revert': {
"str": 'reverted {self.target_link} mod actions',
"icon": 'fa-history',
"color": 'bg-danger'
},
'revoke_app': {
"str": 'revoked an application by {self.target_link}',
"icon": 'fa-robot',
"color": 'bg-muted'
},
'set_flair_locked': {
"str": "set {self.target_link}'s flair (locked)",
"icon": 'fa-award',
"color": 'bg-primary'
},
'set_flair_notlocked': {
"str": "set {self.target_link}'s flair (not locked)",
"icon": 'fa-award',
"color": 'bg-primary'
},
'set_nsfw': {
"str": 'set nsfw on post {self.target_link}',
"icon": 'fa-eye-evil',
"color": 'bg-danger'
},
'shadowban': {
"str": 'shadowbanned {self.target_link}',
"icon": 'fa-eye-slash',
"color": 'bg-danger'
},
'unagendaposter': {
"str": 'removed chud theme from {self.target_link}',
"icon": 'fa-snooze',
"color": 'bg-success'
},
'unban_comment': {
"str": 'reinstated {self.target_link}',
"icon": 'fa-comment',
"color": 'bg-success'
},
'unban_domain': {
"str": 'unbanned a domain',
"icon": 'fa-globe',
"color": 'bg-success'
},
'unban_post': {
"str": 'reinstated post {self.target_link}',
"icon": 'fa-feather-alt',
"color": 'bg-success'
},
'unban_user': {
"str": 'unbanned user {self.target_link}',
"icon": 'fa-user',
"color": 'bg-success'
},
'uncheck': {
"str": 'removed checkmark from {self.target_link}',
"icon": 'fa-badge-check',
"color": 'bg-muted'
},
'undistinguish_comment': {
"str": 'un-distinguished {self.target_link}',
"icon": 'fa-crown',
"color": 'bg-muted'
},
'undistinguish_post': {
"str": 'un-distinguished {self.target_link}',
"icon": 'fa-crown',
"color": 'bg-muted'
},
'unnuke_user': {
"str": 'approved all content of {self.target_link}',
"icon": 'fa-radiation-alt',
"color": 'bg-success'
},
'unpin_comment': {
"str": 'un-pinned a {self.target_link}',
"icon": 'fa-thumbtack fa-rotate--45',
"color": 'bg-muted'
},
'unpin_post': {
"str": 'un-pinned post {self.target_link}',
"icon": 'fa-thumbtack fa-rotate--45',
"color": 'bg-muted'
},
'unset_nsfw': {
"str": 'un-set nsfw on post {self.target_link}',
"icon": 'fa-eye-evil',
"color": 'bg-success'
},
'unshadowban': {
"str": 'unshadowbanned {self.target_link}',
"icon": 'fa-eye',
"color": 'bg-success'
}
}
ACTIONTYPES2 = deepcopy(ACTIONTYPES)
ACTIONTYPES2.pop("shadowban")
ACTIONTYPES2.pop("unshadowban")
ACTIONTYPES2.pop("flair_post")
ACTIONTYPES2.pop("edit_post")

View file

@ -1,16 +1,16 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
class Subscription(Base):
__tablename__ = "subscriptions"
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True)
user = relationship("User", uselist=False, viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
class Subscription(Base):
__tablename__ = "subscriptions"
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True)
user = relationship("User", uselist=False, viewonly=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Subscription(id={self.id})>"

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,15 @@
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
class UserBlock(Base):
__tablename__ = "userblocks"
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
target_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
user = relationship("User", primaryjoin="User.id==UserBlock.user_id", viewonly=True)
target = relationship("User", primaryjoin="User.id==UserBlock.target_id", viewonly=True)
def __repr__(self):
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
class UserBlock(Base):
__tablename__ = "userblocks"
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
target_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
user = relationship("User", primaryjoin="User.id==UserBlock.user_id", viewonly=True)
target = relationship("User", primaryjoin="User.id==UserBlock.target_id", viewonly=True)
def __repr__(self):
return f"<UserBlock(user={self.user_id}, target={self.target_id})>"

View file

@ -1,87 +1,87 @@
from flask import *
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
import time
class Vote(Base):
__tablename__ = "votes"
submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
vote_type = Column(Integer)
app_id = Column(Integer, ForeignKey("oauth_apps.id"))
real = Column(Boolean, default=True)
created_utc = Column(Integer)
user = relationship("User", lazy="subquery", viewonly=True)
post = relationship("Submission", lazy="subquery", viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Vote(id={self.id})>"
@property
@lazy
def json_core(self):
data={
"user_id": self.user_id,
"submission_id":self.submission_id,
"vote_type":self.vote_type
}
return data
@property
@lazy
def json(self):
data=self.json_core
data["user"]=self.user.json_core
data["post"]=self.post.json_core
return data
class CommentVote(Base):
__tablename__ = "commentvotes"
comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
vote_type = Column(Integer)
app_id = Column(Integer, ForeignKey("oauth_apps.id"))
real = Column(Boolean, default=True)
created_utc = Column(Integer)
user = relationship("User", lazy="subquery")
comment = relationship("Comment", lazy="subquery", viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<CommentVote(id={self.id})>"
@property
@lazy
def json_core(self):
data={
"user_id": self.user_id,
"comment_id":self.comment_id,
"vote_type":self.vote_type
}
return data
@property
@lazy
def json(self):
data=self.json_core
data["user"]=self.user.json_core
data["comment"]=self.comment.json_core
from flask import *
from sqlalchemy import *
from sqlalchemy.orm import relationship
from files.__main__ import Base
from files.helpers.lazy import lazy
import time
class Vote(Base):
__tablename__ = "votes"
submission_id = Column(Integer, ForeignKey("submissions.id"), primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
vote_type = Column(Integer)
app_id = Column(Integer, ForeignKey("oauth_apps.id"))
real = Column(Boolean, default=True)
created_utc = Column(Integer)
user = relationship("User", lazy="subquery", viewonly=True)
post = relationship("Submission", lazy="subquery", viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<Vote(id={self.id})>"
@property
@lazy
def json_core(self):
data={
"user_id": self.user_id,
"submission_id":self.submission_id,
"vote_type":self.vote_type
}
return data
@property
@lazy
def json(self):
data=self.json_core
data["user"]=self.user.json_core
data["post"]=self.post.json_core
return data
class CommentVote(Base):
__tablename__ = "commentvotes"
comment_id = Column(Integer, ForeignKey("comments.id"), primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True)
vote_type = Column(Integer)
app_id = Column(Integer, ForeignKey("oauth_apps.id"))
real = Column(Boolean, default=True)
created_utc = Column(Integer)
user = relationship("User", lazy="subquery")
comment = relationship("Comment", lazy="subquery", viewonly=True)
def __init__(self, *args, **kwargs):
if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time())
super().__init__(*args, **kwargs)
def __repr__(self):
return f"<CommentVote(id={self.id})>"
@property
@lazy
def json_core(self):
data={
"user_id": self.user_id,
"comment_id":self.comment_id,
"vote_type":self.vote_type
}
return data
@property
@lazy
def json(self):
data=self.json_core
data["user"]=self.user.json_core
data["comment"]=self.comment.json_core
return data

View file

@ -1,100 +1,100 @@
from files.classes import *
from flask import g
from .sanitize import *
from .const import *
def create_comment(text_html, autojanny=False):
if autojanny: author_id = AUTOJANNY_ID
else: author_id = NOTIFICATIONS_ID
new_comment = Comment(author_id=author_id,
parent_submission=None,
body_html=text_html,
distinguish_level=6)
g.db.add(new_comment)
g.db.flush()
new_comment.top_comment_id = new_comment.id
return new_comment.id
def send_repeatable_notification(uid, text, autojanny=False):
if autojanny: author_id = AUTOJANNY_ID
else: author_id = NOTIFICATIONS_ID
text_html = sanitize(text)
existing_comment = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).first()
if existing_comment:
cid = existing_comment[0]
existing_notif = g.db.query(Notification.user_id).filter_by(user_id=uid, comment_id=cid).one_or_none()
if existing_notif: cid = create_comment(text_html, autojanny)
else: cid = create_comment(text_html, autojanny)
notif = Notification(comment_id=cid, user_id=uid)
g.db.add(notif)
def send_notification(uid, text, autojanny=False):
cid = notif_comment(text, autojanny)
add_notif(cid, uid)
def notif_comment(text, autojanny=False):
if autojanny:
author_id = AUTOJANNY_ID
alert = True
else:
author_id = NOTIFICATIONS_ID
alert = False
text_html = sanitize(text, alert=alert)
existing = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).one_or_none()
if existing: return existing[0]
else: return create_comment(text_html, autojanny)
def notif_comment2(p):
search_html = f'%</a> has mentioned you: <a href="/post/{p.id}">%'
existing = g.db.query(Comment.id).filter(Comment.author_id == NOTIFICATIONS_ID, Comment.parent_submission == None, Comment.body_html.like(search_html)).first()
if existing: return existing[0]
else:
text = f"@{p.author.username} has mentioned you: [{p.title}](/post/{p.id})"
if p.sub: text += f" in <a href='/h/{p.sub}'>/h/{p.sub}"
text_html = sanitize(text, alert=True)
return create_comment(text_html)
def add_notif(cid, uid):
existing = g.db.query(Notification.user_id).filter_by(comment_id=cid, user_id=uid).one_or_none()
if not existing:
notif = Notification(comment_id=cid, user_id=uid)
g.db.add(notif)
def NOTIFY_USERS(text, v):
notify_users = set()
for word, id in NOTIFIED_USERS.items():
if id == 0 or v.id == id: continue
if word in text.lower() and id not in notify_users: notify_users.add(id)
captured = []
for i in mention_regex.finditer(text):
if v.username.lower() == i.group(2).lower(): continue
if i.group(0) in captured: continue
captured.append(i.group(0))
user = get_user(i.group(2), graceful=True)
if user and v.id != user.id and not v.any_block_exists(user): notify_users.add(user.id)
from files.classes import *
from flask import g
from .sanitize import *
from .const import *
def create_comment(text_html, autojanny=False):
if autojanny: author_id = AUTOJANNY_ID
else: author_id = NOTIFICATIONS_ID
new_comment = Comment(author_id=author_id,
parent_submission=None,
body_html=text_html,
distinguish_level=6)
g.db.add(new_comment)
g.db.flush()
new_comment.top_comment_id = new_comment.id
return new_comment.id
def send_repeatable_notification(uid, text, autojanny=False):
if autojanny: author_id = AUTOJANNY_ID
else: author_id = NOTIFICATIONS_ID
text_html = sanitize(text)
existing_comment = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).first()
if existing_comment:
cid = existing_comment[0]
existing_notif = g.db.query(Notification.user_id).filter_by(user_id=uid, comment_id=cid).one_or_none()
if existing_notif: cid = create_comment(text_html, autojanny)
else: cid = create_comment(text_html, autojanny)
notif = Notification(comment_id=cid, user_id=uid)
g.db.add(notif)
def send_notification(uid, text, autojanny=False):
cid = notif_comment(text, autojanny)
add_notif(cid, uid)
def notif_comment(text, autojanny=False):
if autojanny:
author_id = AUTOJANNY_ID
alert = True
else:
author_id = NOTIFICATIONS_ID
alert = False
text_html = sanitize(text, alert=alert)
existing = g.db.query(Comment.id).filter_by(author_id=author_id, parent_submission=None, body_html=text_html).one_or_none()
if existing: return existing[0]
else: return create_comment(text_html, autojanny)
def notif_comment2(p):
search_html = f'%</a> has mentioned you: <a href="/post/{p.id}">%'
existing = g.db.query(Comment.id).filter(Comment.author_id == NOTIFICATIONS_ID, Comment.parent_submission == None, Comment.body_html.like(search_html)).first()
if existing: return existing[0]
else:
text = f"@{p.author.username} has mentioned you: [{p.title}](/post/{p.id})"
if p.sub: text += f" in <a href='/h/{p.sub}'>/h/{p.sub}"
text_html = sanitize(text, alert=True)
return create_comment(text_html)
def add_notif(cid, uid):
existing = g.db.query(Notification.user_id).filter_by(comment_id=cid, user_id=uid).one_or_none()
if not existing:
notif = Notification(comment_id=cid, user_id=uid)
g.db.add(notif)
def NOTIFY_USERS(text, v):
notify_users = set()
for word, id in NOTIFIED_USERS.items():
if id == 0 or v.id == id: continue
if word in text.lower() and id not in notify_users: notify_users.add(id)
captured = []
for i in mention_regex.finditer(text):
if v.username.lower() == i.group(2).lower(): continue
if i.group(0) in captured: continue
captured.append(i.group(0))
user = get_user(i.group(2), graceful=True)
if user and v.id != user.id and not v.any_block_exists(user): notify_users.add(user.id)
return notify_users

View file

@ -1,66 +1,66 @@
from os import environ
import requests
import threading
from .const import *
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN",'').strip()
AUTH = environ.get("DISCORD_AUTH",'').strip()
def discord_wrap(f):
def wrapper(*args, **kwargs):
user=args[0]
if not user.discord_id:
return
thread=threading.Thread(target=f, args=args, kwargs=kwargs)
thread.start()
wrapper.__name__=f.__name__
return wrapper
@discord_wrap
def add_role(user, role_name):
role_id = ROLES[role_name]
url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.put(url, headers=headers, timeout=5)
@discord_wrap
def remove_role(user, role_name):
role_id = ROLES[role_name]
url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.delete(url, headers=headers, timeout=5)
@discord_wrap
def remove_user(user):
url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.delete(url, headers=headers, timeout=5)
@discord_wrap
def set_nick(user, nick):
url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"nick": nick}
requests.patch(url, headers=headers, json=data, timeout=5)
def send_discord_message(message):
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"content": message}
requests.post("https://discordapp.com/api/channels/924485611715452940/messages", headers=headers, data=data, timeout=5)
requests.post("https://discordapp.com/api/channels/924486091795484732/messages", headers=headers, data=data, timeout=5)
def send_cringetopia_message(message):
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"content": message}
from os import environ
import requests
import threading
from .const import *
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN",'').strip()
AUTH = environ.get("DISCORD_AUTH",'').strip()
def discord_wrap(f):
def wrapper(*args, **kwargs):
user=args[0]
if not user.discord_id:
return
thread=threading.Thread(target=f, args=args, kwargs=kwargs)
thread.start()
wrapper.__name__=f.__name__
return wrapper
@discord_wrap
def add_role(user, role_name):
role_id = ROLES[role_name]
url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.put(url, headers=headers, timeout=5)
@discord_wrap
def remove_role(user, role_name):
role_id = ROLES[role_name]
url = f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}/roles/{role_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.delete(url, headers=headers, timeout=5)
@discord_wrap
def remove_user(user):
url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
requests.delete(url, headers=headers, timeout=5)
@discord_wrap
def set_nick(user, nick):
url=f"https://discordapp.com/api/guilds/{SERVER_ID}/members/{user.discord_id}"
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"nick": nick}
requests.patch(url, headers=headers, json=data, timeout=5)
def send_discord_message(message):
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"content": message}
requests.post("https://discordapp.com/api/channels/924485611715452940/messages", headers=headers, data=data, timeout=5)
requests.post("https://discordapp.com/api/channels/924486091795484732/messages", headers=headers, data=data, timeout=5)
def send_cringetopia_message(message):
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
data={"content": message}
requests.post("https://discordapp.com/api/channels/965264044531527740/messages", headers=headers, data=data, timeout=5)

View file

@ -1,289 +1,289 @@
from files.classes import *
from flask import g
def get_id(username, v=None, graceful=False):
username = username.replace('\\', '').replace('_', '\_').replace('%', '').strip()
user = g.db.query(
User.id
).filter(
or_(
User.username.ilike(username),
User.original_username.ilike(username)
)
).one_or_none()
if not user:
if not graceful:
abort(404)
else:
return None
return user[0]
def get_user(username, v=None, graceful=False):
if not username:
if not graceful: abort(404)
else: return None
username = username.replace('\\', '').replace('_', '\_').replace('%', '').strip()
user = g.db.query(
User
).filter(
or_(
User.username.ilike(username),
User.original_username.ilike(username)
)
).one_or_none()
if not user:
if not graceful: abort(404)
else: return None
if v:
block = g.db.query(UserBlock).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == user.id
),
and_(UserBlock.user_id == user.id,
UserBlock.target_id == v.id
)
)
).first()
user.is_blocking = block and block.user_id == v.id
user.is_blocked = block and block.target_id == v.id
return user
def get_account(id, v=None):
try: id = int(id)
except: abort(404)
user = g.db.query(User).filter_by(id = id).one_or_none()
if not user: abort(404)
if v:
block = g.db.query(UserBlock).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == user.id
),
and_(UserBlock.user_id == user.id,
UserBlock.target_id == v.id
)
)
).first()
user.is_blocking = block and block.user_id == v.id
user.is_blocked = block and block.target_id == v.id
return user
def get_post(i, v=None, graceful=False):
if v:
vt = g.db.query(Vote).filter_by(
user_id=v.id, submission_id=i).subquery()
blocking = v.blocking.subquery()
items = g.db.query(
Submission,
vt.c.vote_type,
blocking.c.target_id,
)
items=items.filter(Submission.id == i
).join(
vt,
vt.c.submission_id == Submission.id,
isouter=True
).join(
blocking,
blocking.c.target_id == Submission.author_id,
isouter=True
)
items=items.one_or_none()
if not items:
if graceful: return None
else: abort(404)
x = items[0]
x.voted = items[1] or 0
x.is_blocking = items[2] or 0
else:
items = g.db.query(
Submission
).filter(Submission.id == i).one_or_none()
if not items:
if graceful: return None
else: abort(404)
x=items
return x
def get_posts(pids, v=None):
if not pids:
return []
if v:
vt = g.db.query(Vote).filter(
Vote.submission_id.in_(pids),
Vote.user_id==v.id
).subquery()
blocking = v.blocking.subquery()
blocked = v.blocked.subquery()
query = g.db.query(
Submission,
vt.c.vote_type,
blocking.c.target_id,
blocked.c.target_id,
).filter(
Submission.id.in_(pids)
).join(
vt, vt.c.submission_id==Submission.id, isouter=True
).join(
blocking,
blocking.c.target_id == Submission.author_id,
isouter=True
).join(
blocked,
blocked.c.user_id == Submission.author_id,
isouter=True
).all()
output = [p[0] for p in query]
for i in range(len(output)):
output[i].voted = query[i][1] or 0
output[i].is_blocking = query[i][2] or 0
output[i].is_blocked = query[i][3] or 0
else:
output = g.db.query(Submission,).filter(Submission.id.in_(pids)).all()
return sorted(output, key=lambda x: pids.index(x.id))
def get_comment(i, v=None, graceful=False):
if v:
comment=g.db.query(Comment).filter(Comment.id == i).one_or_none()
if not comment and not graceful: abort(404)
block = g.db.query(UserBlock).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == comment.author_id
),
and_(
UserBlock.user_id == comment.author_id,
UserBlock.target_id == v.id
)
)
).first()
vts = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id)
vt = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
comment.is_blocking = block and block.user_id == v.id
comment.is_blocked = block and block.target_id == v.id
comment.voted = vt.vote_type if vt else 0
else:
comment = g.db.query(Comment).filter(Comment.id == i).one_or_none()
if not comment and not graceful:abort(404)
return comment
def get_comments(cids, v=None, load_parent=False):
if not cids: return []
if v:
votes = g.db.query(CommentVote).filter_by(user_id=v.id).subquery()
blocking = v.blocking.subquery()
blocked = v.blocked.subquery()
comments = g.db.query(
Comment,
votes.c.vote_type,
blocking.c.target_id,
blocked.c.target_id,
).filter(Comment.id.in_(cids))
if not (v and (v.shadowbanned or v.admin_level > 2)):
comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None)
comments = comments.join(
votes,
votes.c.comment_id == Comment.id,
isouter=True
).join(
blocking,
blocking.c.target_id == Comment.author_id,
isouter=True
).join(
blocked,
blocked.c.user_id == Comment.author_id,
isouter=True
).all()
output = []
for c in comments:
comment = c[0]
comment.voted = c[1] or 0
comment.is_blocking = c[2] or 0
comment.is_blocked = c[3] or 0
output.append(comment)
else:
output = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.id.in_(cids)).all()
if load_parent:
parents = [x.parent_comment_id for x in output if x.parent_comment_id]
parents = get_comments(parents, v=v)
parents = {x.id: x for x in parents}
for c in output: c.sex = parents.get(c.parent_comment_id)
return sorted(output, key=lambda x: cids.index(x.id))
def get_domain(s):
parts = s.split(".")
domain_list = set()
for i in range(len(parts)):
new_domain = parts[i]
for j in range(i + 1, len(parts)):
new_domain += "." + parts[j]
domain_list.add(new_domain)
doms = [x for x in g.db.query(BannedDomain).filter(BannedDomain.domain.in_(domain_list)).all()]
if not doms:
return None
doms = sorted(doms, key=lambda x: len(x.domain), reverse=True)
from files.classes import *
from flask import g
def get_id(username, v=None, graceful=False):
username = username.replace('\\', '').replace('_', '\_').replace('%', '').strip()
user = g.db.query(
User.id
).filter(
or_(
User.username.ilike(username),
User.original_username.ilike(username)
)
).one_or_none()
if not user:
if not graceful:
abort(404)
else:
return None
return user[0]
def get_user(username, v=None, graceful=False):
if not username:
if not graceful: abort(404)
else: return None
username = username.replace('\\', '').replace('_', '\_').replace('%', '').strip()
user = g.db.query(
User
).filter(
or_(
User.username.ilike(username),
User.original_username.ilike(username)
)
).one_or_none()
if not user:
if not graceful: abort(404)
else: return None
if v:
block = g.db.query(UserBlock).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == user.id
),
and_(UserBlock.user_id == user.id,
UserBlock.target_id == v.id
)
)
).first()
user.is_blocking = block and block.user_id == v.id
user.is_blocked = block and block.target_id == v.id
return user
def get_account(id, v=None):
try: id = int(id)
except: abort(404)
user = g.db.query(User).filter_by(id = id).one_or_none()
if not user: abort(404)
if v:
block = g.db.query(UserBlock).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == user.id
),
and_(UserBlock.user_id == user.id,
UserBlock.target_id == v.id
)
)
).first()
user.is_blocking = block and block.user_id == v.id
user.is_blocked = block and block.target_id == v.id
return user
def get_post(i, v=None, graceful=False):
if v:
vt = g.db.query(Vote).filter_by(
user_id=v.id, submission_id=i).subquery()
blocking = v.blocking.subquery()
items = g.db.query(
Submission,
vt.c.vote_type,
blocking.c.target_id,
)
items=items.filter(Submission.id == i
).join(
vt,
vt.c.submission_id == Submission.id,
isouter=True
).join(
blocking,
blocking.c.target_id == Submission.author_id,
isouter=True
)
items=items.one_or_none()
if not items:
if graceful: return None
else: abort(404)
x = items[0]
x.voted = items[1] or 0
x.is_blocking = items[2] or 0
else:
items = g.db.query(
Submission
).filter(Submission.id == i).one_or_none()
if not items:
if graceful: return None
else: abort(404)
x=items
return x
def get_posts(pids, v=None):
if not pids:
return []
if v:
vt = g.db.query(Vote).filter(
Vote.submission_id.in_(pids),
Vote.user_id==v.id
).subquery()
blocking = v.blocking.subquery()
blocked = v.blocked.subquery()
query = g.db.query(
Submission,
vt.c.vote_type,
blocking.c.target_id,
blocked.c.target_id,
).filter(
Submission.id.in_(pids)
).join(
vt, vt.c.submission_id==Submission.id, isouter=True
).join(
blocking,
blocking.c.target_id == Submission.author_id,
isouter=True
).join(
blocked,
blocked.c.user_id == Submission.author_id,
isouter=True
).all()
output = [p[0] for p in query]
for i in range(len(output)):
output[i].voted = query[i][1] or 0
output[i].is_blocking = query[i][2] or 0
output[i].is_blocked = query[i][3] or 0
else:
output = g.db.query(Submission,).filter(Submission.id.in_(pids)).all()
return sorted(output, key=lambda x: pids.index(x.id))
def get_comment(i, v=None, graceful=False):
if v:
comment=g.db.query(Comment).filter(Comment.id == i).one_or_none()
if not comment and not graceful: abort(404)
block = g.db.query(UserBlock).filter(
or_(
and_(
UserBlock.user_id == v.id,
UserBlock.target_id == comment.author_id
),
and_(
UserBlock.user_id == comment.author_id,
UserBlock.target_id == v.id
)
)
).first()
vts = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id)
vt = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
comment.is_blocking = block and block.user_id == v.id
comment.is_blocked = block and block.target_id == v.id
comment.voted = vt.vote_type if vt else 0
else:
comment = g.db.query(Comment).filter(Comment.id == i).one_or_none()
if not comment and not graceful:abort(404)
return comment
def get_comments(cids, v=None, load_parent=False):
if not cids: return []
if v:
votes = g.db.query(CommentVote).filter_by(user_id=v.id).subquery()
blocking = v.blocking.subquery()
blocked = v.blocked.subquery()
comments = g.db.query(
Comment,
votes.c.vote_type,
blocking.c.target_id,
blocked.c.target_id,
).filter(Comment.id.in_(cids))
if not (v and (v.shadowbanned or v.admin_level > 2)):
comments = comments.join(User, User.id == Comment.author_id).filter(User.shadowbanned == None)
comments = comments.join(
votes,
votes.c.comment_id == Comment.id,
isouter=True
).join(
blocking,
blocking.c.target_id == Comment.author_id,
isouter=True
).join(
blocked,
blocked.c.user_id == Comment.author_id,
isouter=True
).all()
output = []
for c in comments:
comment = c[0]
comment.voted = c[1] or 0
comment.is_blocking = c[2] or 0
comment.is_blocked = c[3] or 0
output.append(comment)
else:
output = g.db.query(Comment).join(User, User.id == Comment.author_id).filter(User.shadowbanned == None, Comment.id.in_(cids)).all()
if load_parent:
parents = [x.parent_comment_id for x in output if x.parent_comment_id]
parents = get_comments(parents, v=v)
parents = {x.id: x for x in parents}
for c in output: c.sex = parents.get(c.parent_comment_id)
return sorted(output, key=lambda x: cids.index(x.id))
def get_domain(s):
parts = s.split(".")
domain_list = set()
for i in range(len(parts)):
new_domain = parts[i]
for j in range(i + 1, len(parts)):
new_domain += "." + parts[j]
domain_list.add(new_domain)
doms = [x for x in g.db.query(BannedDomain).filter(BannedDomain.domain.in_(domain_list)).all()]
if not doms:
return None
doms = sorted(doms, key=lambda x: len(x.domain), reverse=True)
return doms[0]

View file

@ -1,28 +1,28 @@
from PIL import Image, ImageOps
from PIL.ImageSequence import Iterator
from webptools import gifwebp
import subprocess
def process_image(filename=None, resize=0):
i = Image.open(filename)
if resize and i.width > resize:
try: subprocess.call(["convert", filename, "-coalesce", "-resize", f"{resize}>", filename])
except: pass
elif i.format.lower() != "webp":
exif = i.getexif()
for k in exif.keys():
if k != 0x0112:
exif[k] = None
del exif[k]
i.info["exif"] = exif.tobytes()
if i.format.lower() == "gif":
gifwebp(input_image=filename, output_image=filename, option="-mixed -metadata none -f 100 -mt -m 6")
else:
i = ImageOps.exif_transpose(i)
i.save(filename, format="WEBP", method=6)
from PIL import Image, ImageOps
from PIL.ImageSequence import Iterator
from webptools import gifwebp
import subprocess
def process_image(filename=None, resize=0):
i = Image.open(filename)
if resize and i.width > resize:
try: subprocess.call(["convert", filename, "-coalesce", "-resize", f"{resize}>", filename])
except: pass
elif i.format.lower() != "webp":
exif = i.getexif()
for k in exif.keys():
if k != 0x0112:
exif[k] = None
del exif[k]
i.info["exif"] = exif.tobytes()
if i.format.lower() == "gif":
gifwebp(input_image=filename, output_image=filename, option="-mixed -metadata none -f 100 -mt -m 6")
else:
i = ImageOps.exif_transpose(i)
i.save(filename, format="WEBP", method=6)
return filename

View file

@ -1,18 +1,18 @@
# Prevents certain properties from having to be recomputed each time they
# are referenced
def lazy(f):
def wrapper(*args, **kwargs):
o = args[0]
if "_lazy" not in o.__dict__: o.__dict__["_lazy"] = {}
if f.__name__ not in o.__dict__["_lazy"]: o.__dict__["_lazy"][f.__name__] = f(*args, **kwargs)
return o.__dict__["_lazy"][f.__name__]
wrapper.__name__ = f.__name__
return wrapper
# Prevents certain properties from having to be recomputed each time they
# are referenced
def lazy(f):
def wrapper(*args, **kwargs):
o = args[0]
if "_lazy" not in o.__dict__: o.__dict__["_lazy"] = {}
if f.__name__ not in o.__dict__["_lazy"]: o.__dict__["_lazy"][f.__name__] = f(*args, **kwargs)
return o.__dict__["_lazy"][f.__name__]
wrapper.__name__ = f.__name__
return wrapper

View file

@ -1,328 +1,328 @@
import bleach
from bs4 import BeautifulSoup
from bleach.linkifier import LinkifyFilter, build_url_re
from functools import partial
from .get import *
from os import path, environ
import re
from mistletoe import markdown
from json import loads, dump
from random import random, choice
import signal
import time
import requests
TLDS = ('ac','ad','ae','aero','af','ag','ai','al','am','an','ao','aq','ar','arpa','as','asia','at','au','aw','ax','az','ba','bb','bd','be','bf','bg','bh','bi','biz','bj','bm','bn','bo','br','bs','bt','bv','bw','by','bz','ca','cafe','cat','cc','cd','cf','cg','ch','ci','ck','cl','club','cm','cn','co','com','coop','cr','cu','cv','cx','cy','cz','de','dj','dk','dm','do','dz','ec','edu','ee','eg','er','es','et','eu','fi','fj','fk','fm','fo','fr','ga','gb','gd','ge','gf','gg','gh','gi','gl','gm','gn','gov','gp','gq','gr','gs','gt','gu','gw','gy','hk','hm','hn','hr','ht','hu','id','ie','il','im','in','info','int','io','iq','ir','is','it','je','jm','jo','jobs','jp','ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz','la','lb','lc','li','lk','lr','ls','lt','lu','lv','ly','ma','mc','md','me','mg','mh','mil','mk','ml','mm','mn','mo','mobi','mp','mq','mr','ms','mt','mu','museum','mv','mw','mx','my','mz','na','name','nc','ne','net','nf','ng','ni','nl','no','np','nr','nu','nz','om','org','pa','pe','pf','pg','ph','pk','pl','pm','pn','post','pr','pro','ps','pt','pw','py','qa','re','ro','rs','ru','rw','sa','sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so','social','sr','ss','st','su','sv','sx','sy','sz','tc','td','tel','tf','tg','th','tj','tk','tl','tm','tn','to','tp','tr','travel','tt','tv','tw','tz','ua','ug','uk','us','uy','uz','va','vc','ve','vg','vi','vn','vu','wf','win','ws','xn','xxx','xyz','ye','yt','yu','za','zm','zw')
allowed_tags = ('b','blockquote','br','code','del','em','h1','h2','h3','h4','h5','h6','hr','i','li','ol','p','pre','strong','sub','sup','table','tbody','th','thead','td','tr','ul','marquee','a','span','ruby','rp','rt','spoiler','img','lite-youtube','video','source')
def allowed_attributes(tag, name, value):
if name == 'style': return True
if tag == 'marquee':
if name in ['direction', 'behavior', 'scrollamount']: return True
if name in {'height', 'width'}:
try: value = int(value.replace('px', ''))
except: return False
if 0 < value <= 250: return True
return False
if tag == 'a':
if name == 'href': return True
if name == 'rel' and value == 'nofollow noopener noreferrer': return True
if name == 'target' and value == '_blank': return True
return False
if tag == 'img':
if name in ['src','data-src']:
if value.startswith('/') or value.startswith(f'{SITE_FULL}/') or embed_fullmatch_regex.fullmatch(value): return True
else: return False
if name == 'loading' and value == 'lazy': return True
if name == 'referrpolicy' and value == 'no-referrer': return True
if name == 'data-bs-toggle' and value == 'tooltip': return True
if name in ['alt','title','g','b','pat']: return True
if name == 'class' and value == 'pat-hand': return True
return False
if tag == 'lite-youtube':
if name == 'params' and value.startswith('autoplay=1&modestbranding=1'): return True
if name == 'videoid': return True
return False
if tag == 'video':
if name == 'controls' and value == '': return True
if name == 'preload' and value == 'none': return True
return False
if tag == 'source':
if name == 'src' and embed_fullmatch_regex.fullmatch(value): return True
return False
if tag == 'p':
if name == 'class' and value == 'mb-0': return True
return False
if tag == 'span':
if name == 'class' and value in ['pat-container', 'pat-hand']: return True
if name == 'data-bs-toggle' and value == 'tooltip': return True
if name == 'title': return True
if name == 'alt': return True
return False
url_re = build_url_re(tlds=TLDS, protocols=['http', 'https'])
def callback(attrs, new=False):
href = attrs[(None, "href")]
if not href.startswith('/') and not href.startswith(f'{SITE_FULL}/'):
attrs[(None, "target")] = "_blank"
attrs[(None, "rel")] = "nofollow noopener noreferrer"
return attrs
def handler(signum, frame):
print("Timeout!")
raise Exception("Timeout")
def render_emoji(html, regexp, edit, marseys_used=set(), b=False):
emojis = list(regexp.finditer(html))
captured = set()
for i in emojis:
if i.group(0) in captured: continue
captured.add(i.group(0))
emoji = i.group(1).lower()
attrs = ''
if b: attrs += ' b'
if not edit and len(emojis) <= 20 and random() < 0.0025 and ('marsey' in emoji or emoji in marseys_const2): attrs += ' g'
old = emoji
emoji = emoji.replace('!','').replace('#','')
if emoji == 'marseyrandom': emoji = choice(marseys_const2)
emoji_partial_pat = '<img loading="lazy" alt=":{0}:" src="{1}"{2}>'
emoji_partial = '<img loading="lazy" data-bs-toggle="tooltip" alt=":{0}:" title=":{0}:" src="{1}"{2}>'
emoji_html = None
if emoji.endswith('pat'):
if path.isfile(f"files/assets/images/emojis/{emoji.replace('pat','')}.webp"):
attrs += ' pat'
emoji_html = f'<span class="pat-container" data-bs-toggle="tooltip" alt=":{old}:" title=":{old}:"><img src="/assets/images/hand.webp" class="pat-hand">{emoji_partial_pat.format(old, f"/e/{emoji[:-3]}.webp", attrs)}</span>'
elif emoji.startswith('@'):
if u := get_user(emoji[1:-3], graceful=True):
attrs += ' pat'
emoji_html = f'<span class="pat-container" data-bs-toggle="tooltip" alt=":{old}:" title=":{old}:"><img src="/assets/images/hand.webp" class="pat-hand">{emoji_partial_pat.format(old, f"/pp/{u.id}", attrs)}</span>'
elif path.isfile(f'files/assets/images/emojis/{emoji}.webp'):
emoji_html = emoji_partial.format(old, f'/e/{emoji}.webp', attrs)
if emoji_html:
html = re.sub(f'(?<!"){i.group(0)}', emoji_html, html)
return html
def sanitize(sanitized, alert=False, comment=False, edit=False):
signal.signal(signal.SIGALRM, handler)
signal.alarm(1)
sanitized = linefeeds_regex.sub(r'\1\n\n\2', sanitized)
sanitized = image_regex.sub(r'\1![](\2)\4', sanitized)
sanitized = image_check_regex.sub(r'\1', sanitized)
sanitized = markdown(sanitized)
sanitized = strikethrough_regex.sub(r'<del>\1</del>', sanitized)
sanitized = sanitized.replace('','').replace('','').replace("\ufeff", "").replace("𒐪","")
if alert:
captured = []
for i in mention_regex2.finditer(sanitized):
if i.group(0) in captured: continue
captured.append(i.group(0))
u = get_user(i.group(1), graceful=True)
if u:
sanitized = sanitized.replace(i.group(0), f'''<p><a href="/id/{u.id}"><img loading="lazy" src="/pp/{u.id}">@{u.username}</a>''')
else:
sanitized = reddit_regex.sub(r'\1<a href="https://old.reddit.com/\2" rel="nofollow noopener noreferrer">/\2</a>', sanitized)
sanitized = sub_regex.sub(r'\1<a href="/\2">/\2</a>', sanitized)
captured = []
for i in mention_regex.finditer(sanitized):
if i.group(0) in captured: continue
captured.append(i.group(0))
u = get_user(i.group(2), graceful=True)
if u and (not (g.v and g.v.any_block_exists(u)) or g.v.admin_level > 1):
sanitized = sanitized.replace(i.group(0), f'''{i.group(1)}<a href="/id/{u.id}"><img loading="lazy" src="/pp/{u.id}">@{u.username}</a>''')
sanitized = imgur_regex.sub(r'\1_d.webp?maxwidth=9999&fidelity=high', sanitized)
soup = BeautifulSoup(sanitized, 'lxml')
for tag in soup.find_all("img"):
if tag.get("src") and not tag["src"].startswith('/pp/'):
tag["loading"] = "lazy"
tag["data-src"] = tag["src"]
tag["src"] = "/assets/images/loading.webp"
tag['alt'] = f'![]({tag["data-src"]})'
tag['referrerpolicy'] = "no-referrer"
for tag in soup.find_all("a"):
if tag.get("href") and fishylinks_regex.fullmatch(str(tag.string)):
tag.string = tag["href"]
sanitized = str(soup)
sanitized = spoiler_regex.sub(r'<spoiler>\1</spoiler>', sanitized)
marseys_used = set()
emojis = list(emoji_regex.finditer(sanitized))
if len(emojis) > 20: edit = True
captured = []
for i in emojis:
if i.group(0) in captured: continue
captured.append(i.group(0))
old = i.group(0)
if 'marseylong1' in old or 'marseylong2' in old or 'marseyllama1' in old or 'marseyllama2' in old: new = old.lower().replace(">", " class='mb-0'>")
else: new = old.lower()
new = render_emoji(new, emoji_regex2, edit, marseys_used, True)
sanitized = sanitized.replace(old, new)
emojis = list(emoji_regex2.finditer(sanitized))
if len(emojis) > 20: edit = True
sanitized = render_emoji(sanitized, emoji_regex2, edit, marseys_used)
for rd in ["://reddit.com", "://new.reddit.com", "://www.reddit.com", "://redd.it", "://libredd.it", "://teddit.net"]:
sanitized = sanitized.replace(rd, "://old.reddit.com")
sanitized = sanitized.replace("nitter.net", "twitter.com").replace("old.reddit.com/gallery", "reddit.com/gallery").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").replace("https://streamable.com/", "https://streamable.com/e/").replace("https://youtube.com/shorts/", "https://youtube.com/watch?v=").replace("https://mobile.twitter", "https://twitter").replace("https://m.facebook", "https://facebook").replace("m.wikipedia.org", "wikipedia.org").replace("https://m.youtube", "https://youtube").replace("https://www.youtube", "https://youtube").replace("https://www.twitter", "https://twitter").replace("https://www.instagram", "https://instagram").replace("https://www.tiktok", "https://tiktok")
if "https://youtube.com/watch?v=" in sanitized: sanitized = sanitized.replace("?t=", "&t=")
captured = []
for i in youtube_regex.finditer(sanitized):
if i.group(0) in captured: continue
captured.append(i.group(0))
params = parse_qs(urlparse(i.group(2).replace('&amp;','&')).query)
t = params.get('t', params.get('start', [0]))[0]
if isinstance(t, str): t = t.replace('s','')
htmlsource = f'{i.group(1)}<lite-youtube videoid="{i.group(3)}" params="autoplay=1&modestbranding=1'
if t: htmlsource += f'&start={t}'
htmlsource += '"></lite-youtube>'
sanitized = sanitized.replace(i.group(0), htmlsource)
sanitized = video_sub_regex.sub(r'\1<video controls preload="none"><source src="\2"></video>', sanitized)
if comment:
for marsey in g.db.query(Marsey).filter(Marsey.name.in_(marseys_used)).all():
marsey.count += 1
g.db.add(marsey)
if '#fortune' in sanitized:
sanitized = sanitized.replace('#fortune', '')
sanitized += '\n\n<p>' + choice(FORTUNE_REPLIES) + '</p>'
sanitized = sanitized.replace('&amp;','&')
sanitized = utm_regex.sub('', sanitized)
sanitized = utm_regex2.sub('', sanitized)
sanitized = sanitized.replace('<html><body>','').replace('</body></html>','')
sanitized = bleach.Cleaner(tags=allowed_tags,
attributes=allowed_attributes,
protocols=['http', 'https'],
styles=['color', 'background-color', 'font-weight', 'text-align'],
filters=[partial(LinkifyFilter, skip_tags=["pre"], parse_email=False, callbacks=[callback], url_re=url_re)]
).clean(sanitized)
soup = BeautifulSoup(sanitized, 'lxml')
links = soup.find_all("a")
domain_list = set()
for link in links:
href = link.get("href")
if not href: continue
url = urlparse(href)
domain = url.netloc
url_path = url.path
domain_list.add(domain+url_path)
parts = domain.split(".")
for i in range(len(parts)):
new_domain = parts[i]
for j in range(i + 1, len(parts)):
new_domain += "." + parts[j]
domain_list.add(new_domain)
bans = g.db.query(BannedDomain.domain).filter(BannedDomain.domain.in_(list(domain_list))).all()
if bans: abort(403, description=f"Remove the banned domains {bans} and try again!")
signal.alarm(0)
return sanitized
def allowed_attributes_emojis(tag, name, value):
if tag == 'img':
if name == 'loading' and value == 'lazy': return True
if name == 'data-bs-toggle' and value == 'tooltip': return True
if name in ['src','alt','title','g']: return True
return False
def filter_emojis_only(title, edit=False, graceful=False):
signal.signal(signal.SIGALRM, handler)
signal.alarm(1)
title = title.replace('','').replace('','').replace("\ufeff", "").replace("𒐪","").replace("\n", "").replace("\r", "").replace("\t", "").replace("&", "&amp;").replace('<','&lt;').replace('>','&gt;').replace('"', '&quot;').replace("'", "&#039;").strip()
title = render_emoji(title, emoji_regex3, edit)
title = strikethrough_regex.sub(r'<del>\1</del>', title)
sanitized = bleach.clean(title, tags=['img','del'], attributes=allowed_attributes_emojis, protocols=['http','https'])
signal.alarm(0)
if len(title) > 1500 and not graceful: abort(400)
else: return title
import bleach
from bs4 import BeautifulSoup
from bleach.linkifier import LinkifyFilter, build_url_re
from functools import partial
from .get import *
from os import path, environ
import re
from mistletoe import markdown
from json import loads, dump
from random import random, choice
import signal
import time
import requests
TLDS = ('ac','ad','ae','aero','af','ag','ai','al','am','an','ao','aq','ar','arpa','as','asia','at','au','aw','ax','az','ba','bb','bd','be','bf','bg','bh','bi','biz','bj','bm','bn','bo','br','bs','bt','bv','bw','by','bz','ca','cafe','cat','cc','cd','cf','cg','ch','ci','ck','cl','club','cm','cn','co','com','coop','cr','cu','cv','cx','cy','cz','de','dj','dk','dm','do','dz','ec','edu','ee','eg','er','es','et','eu','fi','fj','fk','fm','fo','fr','ga','gb','gd','ge','gf','gg','gh','gi','gl','gm','gn','gov','gp','gq','gr','gs','gt','gu','gw','gy','hk','hm','hn','hr','ht','hu','id','ie','il','im','in','info','int','io','iq','ir','is','it','je','jm','jo','jobs','jp','ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz','la','lb','lc','li','lk','lr','ls','lt','lu','lv','ly','ma','mc','md','me','mg','mh','mil','mk','ml','mm','mn','mo','mobi','mp','mq','mr','ms','mt','mu','museum','mv','mw','mx','my','mz','na','name','nc','ne','net','nf','ng','ni','nl','no','np','nr','nu','nz','om','org','pa','pe','pf','pg','ph','pk','pl','pm','pn','post','pr','pro','ps','pt','pw','py','qa','re','ro','rs','ru','rw','sa','sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so','social','sr','ss','st','su','sv','sx','sy','sz','tc','td','tel','tf','tg','th','tj','tk','tl','tm','tn','to','tp','tr','travel','tt','tv','tw','tz','ua','ug','uk','us','uy','uz','va','vc','ve','vg','vi','vn','vu','wf','win','ws','xn','xxx','xyz','ye','yt','yu','za','zm','zw')
allowed_tags = ('b','blockquote','br','code','del','em','h1','h2','h3','h4','h5','h6','hr','i','li','ol','p','pre','strong','sub','sup','table','tbody','th','thead','td','tr','ul','marquee','a','span','ruby','rp','rt','spoiler','img','lite-youtube','video','source')
def allowed_attributes(tag, name, value):
if name == 'style': return True
if tag == 'marquee':
if name in ['direction', 'behavior', 'scrollamount']: return True
if name in {'height', 'width'}:
try: value = int(value.replace('px', ''))
except: return False
if 0 < value <= 250: return True
return False
if tag == 'a':
if name == 'href': return True
if name == 'rel' and value == 'nofollow noopener noreferrer': return True
if name == 'target' and value == '_blank': return True
return False
if tag == 'img':
if name in ['src','data-src']:
if value.startswith('/') or value.startswith(f'{SITE_FULL}/') or embed_fullmatch_regex.fullmatch(value): return True
else: return False
if name == 'loading' and value == 'lazy': return True
if name == 'referrpolicy' and value == 'no-referrer': return True
if name == 'data-bs-toggle' and value == 'tooltip': return True
if name in ['alt','title','g','b','pat']: return True
if name == 'class' and value == 'pat-hand': return True
return False
if tag == 'lite-youtube':
if name == 'params' and value.startswith('autoplay=1&modestbranding=1'): return True
if name == 'videoid': return True
return False
if tag == 'video':
if name == 'controls' and value == '': return True
if name == 'preload' and value == 'none': return True
return False
if tag == 'source':
if name == 'src' and embed_fullmatch_regex.fullmatch(value): return True
return False
if tag == 'p':
if name == 'class' and value == 'mb-0': return True
return False
if tag == 'span':
if name == 'class' and value in ['pat-container', 'pat-hand']: return True
if name == 'data-bs-toggle' and value == 'tooltip': return True
if name == 'title': return True
if name == 'alt': return True
return False
url_re = build_url_re(tlds=TLDS, protocols=['http', 'https'])
def callback(attrs, new=False):
href = attrs[(None, "href")]
if not href.startswith('/') and not href.startswith(f'{SITE_FULL}/'):
attrs[(None, "target")] = "_blank"
attrs[(None, "rel")] = "nofollow noopener noreferrer"
return attrs
def handler(signum, frame):
print("Timeout!")
raise Exception("Timeout")
def render_emoji(html, regexp, edit, marseys_used=set(), b=False):
emojis = list(regexp.finditer(html))
captured = set()
for i in emojis:
if i.group(0) in captured: continue
captured.add(i.group(0))
emoji = i.group(1).lower()
attrs = ''
if b: attrs += ' b'
if not edit and len(emojis) <= 20 and random() < 0.0025 and ('marsey' in emoji or emoji in marseys_const2): attrs += ' g'
old = emoji
emoji = emoji.replace('!','').replace('#','')
if emoji == 'marseyrandom': emoji = choice(marseys_const2)
emoji_partial_pat = '<img loading="lazy" alt=":{0}:" src="{1}"{2}>'
emoji_partial = '<img loading="lazy" data-bs-toggle="tooltip" alt=":{0}:" title=":{0}:" src="{1}"{2}>'
emoji_html = None
if emoji.endswith('pat'):
if path.isfile(f"files/assets/images/emojis/{emoji.replace('pat','')}.webp"):
attrs += ' pat'
emoji_html = f'<span class="pat-container" data-bs-toggle="tooltip" alt=":{old}:" title=":{old}:"><img src="/assets/images/hand.webp" class="pat-hand">{emoji_partial_pat.format(old, f"/e/{emoji[:-3]}.webp", attrs)}</span>'
elif emoji.startswith('@'):
if u := get_user(emoji[1:-3], graceful=True):
attrs += ' pat'
emoji_html = f'<span class="pat-container" data-bs-toggle="tooltip" alt=":{old}:" title=":{old}:"><img src="/assets/images/hand.webp" class="pat-hand">{emoji_partial_pat.format(old, f"/pp/{u.id}", attrs)}</span>'
elif path.isfile(f'files/assets/images/emojis/{emoji}.webp'):
emoji_html = emoji_partial.format(old, f'/e/{emoji}.webp', attrs)
if emoji_html:
html = re.sub(f'(?<!"){i.group(0)}', emoji_html, html)
return html
def sanitize(sanitized, alert=False, comment=False, edit=False):
signal.signal(signal.SIGALRM, handler)
signal.alarm(1)
sanitized = linefeeds_regex.sub(r'\1\n\n\2', sanitized)
sanitized = image_regex.sub(r'\1![](\2)\4', sanitized)
sanitized = image_check_regex.sub(r'\1', sanitized)
sanitized = markdown(sanitized)
sanitized = strikethrough_regex.sub(r'<del>\1</del>', sanitized)
sanitized = sanitized.replace('','').replace('','').replace("\ufeff", "").replace("𒐪","")
if alert:
captured = []
for i in mention_regex2.finditer(sanitized):
if i.group(0) in captured: continue
captured.append(i.group(0))
u = get_user(i.group(1), graceful=True)
if u:
sanitized = sanitized.replace(i.group(0), f'''<p><a href="/id/{u.id}"><img loading="lazy" src="/pp/{u.id}">@{u.username}</a>''')
else:
sanitized = reddit_regex.sub(r'\1<a href="https://old.reddit.com/\2" rel="nofollow noopener noreferrer">/\2</a>', sanitized)
sanitized = sub_regex.sub(r'\1<a href="/\2">/\2</a>', sanitized)
captured = []
for i in mention_regex.finditer(sanitized):
if i.group(0) in captured: continue
captured.append(i.group(0))
u = get_user(i.group(2), graceful=True)
if u and (not (g.v and g.v.any_block_exists(u)) or g.v.admin_level > 1):
sanitized = sanitized.replace(i.group(0), f'''{i.group(1)}<a href="/id/{u.id}"><img loading="lazy" src="/pp/{u.id}">@{u.username}</a>''')
sanitized = imgur_regex.sub(r'\1_d.webp?maxwidth=9999&fidelity=high', sanitized)
soup = BeautifulSoup(sanitized, 'lxml')
for tag in soup.find_all("img"):
if tag.get("src") and not tag["src"].startswith('/pp/'):
tag["loading"] = "lazy"
tag["data-src"] = tag["src"]
tag["src"] = "/assets/images/loading.webp"
tag['alt'] = f'![]({tag["data-src"]})'
tag['referrerpolicy'] = "no-referrer"
for tag in soup.find_all("a"):
if tag.get("href") and fishylinks_regex.fullmatch(str(tag.string)):
tag.string = tag["href"]
sanitized = str(soup)
sanitized = spoiler_regex.sub(r'<spoiler>\1</spoiler>', sanitized)
marseys_used = set()
emojis = list(emoji_regex.finditer(sanitized))
if len(emojis) > 20: edit = True
captured = []
for i in emojis:
if i.group(0) in captured: continue
captured.append(i.group(0))
old = i.group(0)
if 'marseylong1' in old or 'marseylong2' in old or 'marseyllama1' in old or 'marseyllama2' in old: new = old.lower().replace(">", " class='mb-0'>")
else: new = old.lower()
new = render_emoji(new, emoji_regex2, edit, marseys_used, True)
sanitized = sanitized.replace(old, new)
emojis = list(emoji_regex2.finditer(sanitized))
if len(emojis) > 20: edit = True
sanitized = render_emoji(sanitized, emoji_regex2, edit, marseys_used)
for rd in ["://reddit.com", "://new.reddit.com", "://www.reddit.com", "://redd.it", "://libredd.it", "://teddit.net"]:
sanitized = sanitized.replace(rd, "://old.reddit.com")
sanitized = sanitized.replace("nitter.net", "twitter.com").replace("old.reddit.com/gallery", "reddit.com/gallery").replace("https://youtu.be/", "https://youtube.com/watch?v=").replace("https://music.youtube.com/watch?v=", "https://youtube.com/watch?v=").replace("https://streamable.com/", "https://streamable.com/e/").replace("https://youtube.com/shorts/", "https://youtube.com/watch?v=").replace("https://mobile.twitter", "https://twitter").replace("https://m.facebook", "https://facebook").replace("m.wikipedia.org", "wikipedia.org").replace("https://m.youtube", "https://youtube").replace("https://www.youtube", "https://youtube").replace("https://www.twitter", "https://twitter").replace("https://www.instagram", "https://instagram").replace("https://www.tiktok", "https://tiktok")
if "https://youtube.com/watch?v=" in sanitized: sanitized = sanitized.replace("?t=", "&t=")
captured = []
for i in youtube_regex.finditer(sanitized):
if i.group(0) in captured: continue
captured.append(i.group(0))
params = parse_qs(urlparse(i.group(2).replace('&amp;','&')).query)
t = params.get('t', params.get('start', [0]))[0]
if isinstance(t, str): t = t.replace('s','')
htmlsource = f'{i.group(1)}<lite-youtube videoid="{i.group(3)}" params="autoplay=1&modestbranding=1'
if t: htmlsource += f'&start={t}'
htmlsource += '"></lite-youtube>'
sanitized = sanitized.replace(i.group(0), htmlsource)
sanitized = video_sub_regex.sub(r'\1<video controls preload="none"><source src="\2"></video>', sanitized)
if comment:
for marsey in g.db.query(Marsey).filter(Marsey.name.in_(marseys_used)).all():
marsey.count += 1
g.db.add(marsey)
if '#fortune' in sanitized:
sanitized = sanitized.replace('#fortune', '')
sanitized += '\n\n<p>' + choice(FORTUNE_REPLIES) + '</p>'
sanitized = sanitized.replace('&amp;','&')
sanitized = utm_regex.sub('', sanitized)
sanitized = utm_regex2.sub('', sanitized)
sanitized = sanitized.replace('<html><body>','').replace('</body></html>','')
sanitized = bleach.Cleaner(tags=allowed_tags,
attributes=allowed_attributes,
protocols=['http', 'https'],
styles=['color', 'background-color', 'font-weight', 'text-align'],
filters=[partial(LinkifyFilter, skip_tags=["pre"], parse_email=False, callbacks=[callback], url_re=url_re)]
).clean(sanitized)
soup = BeautifulSoup(sanitized, 'lxml')
links = soup.find_all("a")
domain_list = set()
for link in links:
href = link.get("href")
if not href: continue
url = urlparse(href)
domain = url.netloc
url_path = url.path
domain_list.add(domain+url_path)
parts = domain.split(".")
for i in range(len(parts)):
new_domain = parts[i]
for j in range(i + 1, len(parts)):
new_domain += "." + parts[j]
domain_list.add(new_domain)
bans = g.db.query(BannedDomain.domain).filter(BannedDomain.domain.in_(list(domain_list))).all()
if bans: abort(403, description=f"Remove the banned domains {bans} and try again!")
signal.alarm(0)
return sanitized
def allowed_attributes_emojis(tag, name, value):
if tag == 'img':
if name == 'loading' and value == 'lazy': return True
if name == 'data-bs-toggle' and value == 'tooltip': return True
if name in ['src','alt','title','g']: return True
return False
def filter_emojis_only(title, edit=False, graceful=False):
signal.signal(signal.SIGALRM, handler)
signal.alarm(1)
title = title.replace('','').replace('','').replace("\ufeff", "").replace("𒐪","").replace("\n", "").replace("\r", "").replace("\t", "").replace("&", "&amp;").replace('<','&lt;').replace('>','&gt;').replace('"', '&quot;').replace("'", "&#039;").strip()
title = render_emoji(title, emoji_regex3, edit)
title = strikethrough_regex.sub(r'<del>\1</del>', title)
sanitized = bleach.clean(title, tags=['img','del'], attributes=allowed_attributes_emojis, protocols=['http','https'])
signal.alarm(0)
if len(title) > 1500 and not graceful: abort(400)
else: return title

View file

@ -1,23 +1,23 @@
from werkzeug.security import *
from os import environ
def generate_hash(string):
msg = bytes(string, "utf-16")
return hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"),
msg=msg,
digestmod='md5'
).hexdigest()
def validate_hash(string, hashstr):
return hmac.compare_digest(hashstr, generate_hash(string))
def hash_password(password):
return generate_password_hash(
password, method='pbkdf2:sha512', salt_length=8)
from werkzeug.security import *
from os import environ
def generate_hash(string):
msg = bytes(string, "utf-16")
return hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"),
msg=msg,
digestmod='md5'
).hexdigest()
def validate_hash(string, hashstr):
return hmac.compare_digest(hashstr, generate_hash(string))
def hash_password(password):
return generate_password_hash(
password, method='pbkdf2:sha512', salt_length=8)

View file

@ -1,118 +1,118 @@
from .get import *
from .alerts import *
from files.helpers.const import *
from files.__main__ import db_session
from random import randint
def get_logged_in_user():
if not (hasattr(g, 'db') and g.db): g.db = db_session()
v = None
token = request.headers.get("Authorization","").strip()
if token:
client = g.db.query(ClientAuth).filter(ClientAuth.access_token == token).one_or_none()
if client:
v = client.user
v.client = client
else:
lo_user = session.get("lo_user")
if lo_user:
id = int(lo_user)
v = g.db.query(User).filter_by(id=id).one_or_none()
if v:
nonce = session.get("login_nonce", 0)
if nonce < v.login_nonce or v.id != id: abort(401)
if request.method != "GET":
submitted_key = request.values.get("formkey")
if not submitted_key: abort(401)
if not v.validate_formkey(submitted_key): abort(401)
v.client = None
if request.method.lower() != "get" and app.config['SETTINGS']['Read-only mode'] and not (v and v.admin_level):
abort(403)
if v and v.patron:
if request.content_length and request.content_length > 16 * 1024 * 1024: abort(413)
elif request.content_length and request.content_length > 8 * 1024 * 1024: abort(413)
return v
def check_ban_evade(v):
if v and not v.patron and v.admin_level < 2 and v.ban_evade and not v.unban_utc:
v.shadowbanned = "AutoJanny"
g.db.add(v)
g.db.commit()
def auth_desired(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
check_ban_evade(v)
g.v = v
return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__
return wrapper
def auth_required(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
check_ban_evade(v)
g.v = v
return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__
return wrapper
def is_not_permabanned(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
check_ban_evade(v)
if v.is_banned and v.unban_utc == 0:
return {"error": "Interal server error"}, 500
g.v = v
return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__
return wrapper
def admin_level_required(x):
def wrapper_maker(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
if v.admin_level < x: abort(403)
g.v = v
return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__
return wrapper
from .get import *
from .alerts import *
from files.helpers.const import *
from files.__main__ import db_session
from random import randint
def get_logged_in_user():
if not (hasattr(g, 'db') and g.db): g.db = db_session()
v = None
token = request.headers.get("Authorization","").strip()
if token:
client = g.db.query(ClientAuth).filter(ClientAuth.access_token == token).one_or_none()
if client:
v = client.user
v.client = client
else:
lo_user = session.get("lo_user")
if lo_user:
id = int(lo_user)
v = g.db.query(User).filter_by(id=id).one_or_none()
if v:
nonce = session.get("login_nonce", 0)
if nonce < v.login_nonce or v.id != id: abort(401)
if request.method != "GET":
submitted_key = request.values.get("formkey")
if not submitted_key: abort(401)
if not v.validate_formkey(submitted_key): abort(401)
v.client = None
if request.method.lower() != "get" and app.config['SETTINGS']['Read-only mode'] and not (v and v.admin_level):
abort(403)
if v and v.patron:
if request.content_length and request.content_length > 16 * 1024 * 1024: abort(413)
elif request.content_length and request.content_length > 8 * 1024 * 1024: abort(413)
return v
def check_ban_evade(v):
if v and not v.patron and v.admin_level < 2 and v.ban_evade and not v.unban_utc:
v.shadowbanned = "AutoJanny"
g.db.add(v)
g.db.commit()
def auth_desired(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
check_ban_evade(v)
g.v = v
return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__
return wrapper
def auth_required(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
check_ban_evade(v)
g.v = v
return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__
return wrapper
def is_not_permabanned(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
check_ban_evade(v)
if v.is_banned and v.unban_utc == 0:
return {"error": "Interal server error"}, 500
g.v = v
return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__
return wrapper
def admin_level_required(x):
def wrapper_maker(f):
def wrapper(*args, **kwargs):
v = get_logged_in_user()
if not v: abort(401)
if v.admin_level < x: abort(403)
g.v = v
return make_response(f(*args, v=v, **kwargs))
wrapper.__name__ = f.__name__
return wrapper
return wrapper_maker

View file

@ -1,93 +1,93 @@
from os import environ
import time
from flask import *
from urllib.parse import quote
from files.helpers.security import *
from files.helpers.wrappers import *
from files.helpers.const import *
from files.classes import *
from files.__main__ import app, mail, limiter
from flask_mail import Message
name = environ.get("SITE_NAME").strip()
def send_mail(to_address, subject, html):
msg = Message(html=html, subject=subject, sender=f"{name}@{SITE}", recipients=[to_address])
mail.send(msg)
def send_verification_email(user, email=None):
if not email:
email = user.email
url = f"https://{app.config['SERVER_NAME']}/activate"
now = int(time.time())
token = generate_hash(f"{email}+{user.id}+{now}")
params = f"?email={quote(email)}&id={user.id}&time={now}&token={token}"
link = url + params
send_mail(to_address=email,
html=render_template("email/email_verify.html",
action_url=link,
v=user),
subject=f"Validate your {name} account email."
)
@app.post("/verify_email")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def api_verify_email(v):
send_verification_email(v)
return {"message": "Email has been sent (ETA ~5 minutes)"}
@app.get("/activate")
@auth_required
def activate(v):
email = request.values.get("email", "").strip().lower()
if not email_regex.fullmatch(email):
return render_template("message.html", v=v, title="Invalid email.", error="Invalid email."), 400
id = request.values.get("id", "").strip()
timestamp = int(request.values.get("time", "0"))
token = request.values.get("token", "").strip()
if int(time.time()) - timestamp > 3600:
return render_template("message.html", v=v, title="Verification link expired.",
message="That link has expired. Visit your settings to send yourself another verification email."), 410
if not validate_hash(f"{email}+{id}+{timestamp}", token):
abort(403)
user = g.db.query(User).filter_by(id=id).one_or_none()
if not user:
abort(404)
if user.is_activated and user.email == email:
return render_template("message_success.html", v=v, title="Email already verified.", message="Email already verified."), 404
user.email = email
user.is_activated = True
if not any(b.badge_id == 2 for b in user.badges):
mail_badge = Badge(user_id=user.id, badge_id=2)
g.db.add(mail_badge)
g.db.flush()
send_notification(user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({mail_badge.path})\n\n{mail_badge.name}")
g.db.add(user)
g.db.commit()
return render_template("message_success.html", v=v, title="Email verified.", message=f"Your email {email} has been verified. Thank you.")
from os import environ
import time
from flask import *
from urllib.parse import quote
from files.helpers.security import *
from files.helpers.wrappers import *
from files.helpers.const import *
from files.classes import *
from files.__main__ import app, mail, limiter
from flask_mail import Message
name = environ.get("SITE_NAME").strip()
def send_mail(to_address, subject, html):
msg = Message(html=html, subject=subject, sender=f"{name}@{SITE}", recipients=[to_address])
mail.send(msg)
def send_verification_email(user, email=None):
if not email:
email = user.email
url = f"https://{app.config['SERVER_NAME']}/activate"
now = int(time.time())
token = generate_hash(f"{email}+{user.id}+{now}")
params = f"?email={quote(email)}&id={user.id}&time={now}&token={token}"
link = url + params
send_mail(to_address=email,
html=render_template("email/email_verify.html",
action_url=link,
v=user),
subject=f"Validate your {name} account email."
)
@app.post("/verify_email")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def api_verify_email(v):
send_verification_email(v)
return {"message": "Email has been sent (ETA ~5 minutes)"}
@app.get("/activate")
@auth_required
def activate(v):
email = request.values.get("email", "").strip().lower()
if not email_regex.fullmatch(email):
return render_template("message.html", v=v, title="Invalid email.", error="Invalid email."), 400
id = request.values.get("id", "").strip()
timestamp = int(request.values.get("time", "0"))
token = request.values.get("token", "").strip()
if int(time.time()) - timestamp > 3600:
return render_template("message.html", v=v, title="Verification link expired.",
message="That link has expired. Visit your settings to send yourself another verification email."), 410
if not validate_hash(f"{email}+{id}+{timestamp}", token):
abort(403)
user = g.db.query(User).filter_by(id=id).one_or_none()
if not user:
abort(404)
if user.is_activated and user.email == email:
return render_template("message_success.html", v=v, title="Email already verified.", message="Email already verified."), 404
user.email = email
user.is_activated = True
if not any(b.badge_id == 2 for b in user.badges):
mail_badge = Badge(user_id=user.id, badge_id=2)
g.db.add(mail_badge)
g.db.flush()
send_notification(user.id, f"@AutoJanny has given you the following profile badge:\n\n![]({mail_badge.path})\n\n{mail_badge.name}")
g.db.add(user)
g.db.commit()
return render_template("message_success.html", v=v, title="Email verified.", message=f"Your email {email} has been verified. Thank you.")

View file

@ -1,18 +1,18 @@
from .admin import *
from .comments import *
from .discord import *
from .errors import *
from .reporting import *
from .front import *
from .login import *
from .oauth import *
from .posts import *
from .search import *
from .settings import *
from .static import *
from .users import *
from .votes import *
from .feeds import *
from .awards import *
from .giphy import *
from .admin import *
from .comments import *
from .discord import *
from .errors import *
from .reporting import *
from .front import *
from .login import *
from .oauth import *
from .posts import *
from .search import *
from .settings import *
from .static import *
from .users import *
from .votes import *
from .feeds import *
from .awards import *
from .giphy import *
from .subs import *

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,138 +1,138 @@
from files.helpers.wrappers import *
from files.helpers.security import *
from files.helpers.discord import add_role
from files.__main__ import app
import requests
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN").strip()
DISCORD_ENDPOINT = "https://discordapp.com/api/v6"
WELCOME_CHANNEL="846509313941700618"
@app.get("/discord")
@is_not_permabanned
def join_discord(v):
if v.shadowbanned: return {"error": "Internal server error"}
now=int(time.time())
state=generate_hash(f"{now}+{v.id}+discord")
state=f"{now}.{state}"
return redirect(f"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri=https%3A%2F%2F{app.config['SERVER_NAME']}%2Fdiscord_redirect&response_type=code&scope=identify%20guilds.join&state={state}")
@app.get("/discord_redirect")
@auth_required
def discord_redirect(v):
now=int(time.time())
state=request.values.get('state','').split('.')
timestamp=state[0]
state=state[1]
if int(timestamp) < now-600:
abort(400)
if not validate_hash(f"{timestamp}+{v.id}+discord", state):
abort(400)
code = request.values.get("code","")
if not code:
abort(400)
data={
"client_id":CLIENT_ID,
'client_secret': CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': f"https://{app.config['SERVER_NAME']}/discord_redirect",
'scope': 'identify guilds.join'
}
headers={
'Content-Type': 'application/x-www-form-urlencoded'
}
url="https://discord.com/api/oauth2/token"
x=requests.post(url, headers=headers, data=data, timeout=5)
x=x.json()
token=x["access_token"]
url="https://discord.com/api/users/@me"
headers={
'Authorization': f"Bearer {token}"
}
x=requests.get(url, headers=headers, timeout=5)
x=x.json()
headers={
'Authorization': f"Bot {BOT_TOKEN}",
'Content-Type': "application/json"
}
if v.discord_id and v.discord_id != x['id']:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
requests.delete(url, headers=headers, timeout=5)
if g.db.query(User).filter(User.id!=v.id, User.discord_id==x["id"]).one_or_none():
return render_template("message.html", title="Discord account already linked.", error="That Discord account is already in use by another user.", v=v)
v.discord_id=x["id"]
g.db.add(v)
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{x['id']}"
name=v.username
data={
"access_token":token,
"nick":name,
}
x=requests.put(url, headers=headers, json=data, timeout=5)
if x.status_code in {201, 204}:
if v.admin_level > 2:
add_role(v, "owner")
time.sleep(0.1)
if v.admin_level > 1: add_role(v, "admin")
time.sleep(0.1)
add_role(v, "linked")
if v.patron:
time.sleep(0.1)
add_role(v, str(v.patron))
else:
return x.json()
if x.status_code==204:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
data={
"nick": name
}
requests.patch(url, headers=headers, json=data, timeout=5)
g.db.commit()
from files.helpers.wrappers import *
from files.helpers.security import *
from files.helpers.discord import add_role
from files.__main__ import app
import requests
SERVER_ID = environ.get("DISCORD_SERVER_ID",'').strip()
CLIENT_ID = environ.get("DISCORD_CLIENT_ID",'').strip()
CLIENT_SECRET = environ.get("DISCORD_CLIENT_SECRET",'').strip()
BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN").strip()
DISCORD_ENDPOINT = "https://discordapp.com/api/v6"
WELCOME_CHANNEL="846509313941700618"
@app.get("/discord")
@is_not_permabanned
def join_discord(v):
if v.shadowbanned: return {"error": "Internal server error"}
now=int(time.time())
state=generate_hash(f"{now}+{v.id}+discord")
state=f"{now}.{state}"
return redirect(f"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri=https%3A%2F%2F{app.config['SERVER_NAME']}%2Fdiscord_redirect&response_type=code&scope=identify%20guilds.join&state={state}")
@app.get("/discord_redirect")
@auth_required
def discord_redirect(v):
now=int(time.time())
state=request.values.get('state','').split('.')
timestamp=state[0]
state=state[1]
if int(timestamp) < now-600:
abort(400)
if not validate_hash(f"{timestamp}+{v.id}+discord", state):
abort(400)
code = request.values.get("code","")
if not code:
abort(400)
data={
"client_id":CLIENT_ID,
'client_secret': CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': f"https://{app.config['SERVER_NAME']}/discord_redirect",
'scope': 'identify guilds.join'
}
headers={
'Content-Type': 'application/x-www-form-urlencoded'
}
url="https://discord.com/api/oauth2/token"
x=requests.post(url, headers=headers, data=data, timeout=5)
x=x.json()
token=x["access_token"]
url="https://discord.com/api/users/@me"
headers={
'Authorization': f"Bearer {token}"
}
x=requests.get(url, headers=headers, timeout=5)
x=x.json()
headers={
'Authorization': f"Bot {BOT_TOKEN}",
'Content-Type': "application/json"
}
if v.discord_id and v.discord_id != x['id']:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
requests.delete(url, headers=headers, timeout=5)
if g.db.query(User).filter(User.id!=v.id, User.discord_id==x["id"]).one_or_none():
return render_template("message.html", title="Discord account already linked.", error="That Discord account is already in use by another user.", v=v)
v.discord_id=x["id"]
g.db.add(v)
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{x['id']}"
name=v.username
data={
"access_token":token,
"nick":name,
}
x=requests.put(url, headers=headers, json=data, timeout=5)
if x.status_code in {201, 204}:
if v.admin_level > 2:
add_role(v, "owner")
time.sleep(0.1)
if v.admin_level > 1: add_role(v, "admin")
time.sleep(0.1)
add_role(v, "linked")
if v.patron:
time.sleep(0.1)
add_role(v, str(v.patron))
else:
return x.json()
if x.status_code==204:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
data={
"nick": name
}
requests.patch(url, headers=headers, json=data, timeout=5)
g.db.commit()
return redirect(f"https://discord.com/channels/{SERVER_ID}/{WELCOME_CHANNEL}")

View file

@ -1,76 +1,76 @@
from files.helpers.wrappers import *
from flask import *
from urllib.parse import quote, urlencode
import time
from files.__main__ import app, limiter
@app.errorhandler(400)
def error_400(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "400 Bad Request"}, 400
else: return render_template('errors/400.html', err=True), 400
@app.errorhandler(401)
def error_401(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "401 Not Authorized"}, 401
else:
path = request.path
qs = urlencode(dict(request.values))
argval = quote(f"{path}?{qs}", safe='')
return redirect(f"/login?redirect={argval}")
@app.errorhandler(403)
def error_403(e):
description = e.description
if description == "You don't have the permission to access the requested resource. It is either read-protected or not readable by the server.": description = ''
if request.headers.get("Authorization") or request.headers.get("xhr"):
if not description: description = "403 Forbidden"
return {"error": description}, 403
else:
if not description: description = "YOU AREN'T WELCOME HERE GO AWAY"
return render_template('errors/403.html', description=description, err=True), 403
@app.errorhandler(404)
def error_404(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "404 Not Found"}, 404
else: return render_template('errors/404.html', err=True), 404
@app.errorhandler(405)
def error_405(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "405 Method Not Allowed"}, 405
else: return render_template('errors/405.html', err=True), 405
@app.errorhandler(413)
def error_413(e):
return {"error": "Max file size is 8 MB (16 MB for paypigs)"}, 413
if request.headers.get("Authorization") or request.headers.get("xhr"):
return {"error": "Max file size is 8 MB (16 MB for paypigs)"}, 413
else: return render_template('errors/413.html', err=True), 413
@app.errorhandler(429)
def error_429(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "429 Too Many Requests"}, 429
else: return render_template('errors/429.html', err=True), 429
@app.errorhandler(500)
def error_500(e):
g.db.rollback()
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "500 Internal Server Error"}, 500
else: return render_template('errors/500.html', err=True), 500
@app.post("/allow_nsfw")
def allow_nsfw():
session["over_18"] = int(time.time()) + 3600
redir = request.values.get("redir")
if redir:
if redir.startswith(f'{SITE_FULL}/'): return redirect(redir)
if redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}')
from files.helpers.wrappers import *
from flask import *
from urllib.parse import quote, urlencode
import time
from files.__main__ import app, limiter
@app.errorhandler(400)
def error_400(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "400 Bad Request"}, 400
else: return render_template('errors/400.html', err=True), 400
@app.errorhandler(401)
def error_401(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "401 Not Authorized"}, 401
else:
path = request.path
qs = urlencode(dict(request.values))
argval = quote(f"{path}?{qs}", safe='')
return redirect(f"/login?redirect={argval}")
@app.errorhandler(403)
def error_403(e):
description = e.description
if description == "You don't have the permission to access the requested resource. It is either read-protected or not readable by the server.": description = ''
if request.headers.get("Authorization") or request.headers.get("xhr"):
if not description: description = "403 Forbidden"
return {"error": description}, 403
else:
if not description: description = "YOU AREN'T WELCOME HERE GO AWAY"
return render_template('errors/403.html', description=description, err=True), 403
@app.errorhandler(404)
def error_404(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "404 Not Found"}, 404
else: return render_template('errors/404.html', err=True), 404
@app.errorhandler(405)
def error_405(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "405 Method Not Allowed"}, 405
else: return render_template('errors/405.html', err=True), 405
@app.errorhandler(413)
def error_413(e):
return {"error": "Max file size is 8 MB (16 MB for paypigs)"}, 413
if request.headers.get("Authorization") or request.headers.get("xhr"):
return {"error": "Max file size is 8 MB (16 MB for paypigs)"}, 413
else: return render_template('errors/413.html', err=True), 413
@app.errorhandler(429)
def error_429(e):
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "429 Too Many Requests"}, 429
else: return render_template('errors/429.html', err=True), 429
@app.errorhandler(500)
def error_500(e):
g.db.rollback()
if request.headers.get("Authorization") or request.headers.get("xhr"): return {"error": "500 Internal Server Error"}, 500
else: return render_template('errors/500.html', err=True), 500
@app.post("/allow_nsfw")
def allow_nsfw():
session["over_18"] = int(time.time()) + 3600
redir = request.values.get("redir")
if redir:
if redir.startswith(f'{SITE_FULL}/'): return redirect(redir)
if redir.startswith('/'): return redirect(f'{SITE_FULL}{redir}')
return redirect('/')

View file

@ -1,69 +1,69 @@
import html
from .front import frontlist
from datetime import datetime
from files.helpers.get import *
from yattag import Doc
from files.helpers.wrappers import *
from files.helpers.jinja2 import *
from files.__main__ import app
@app.get('/rss/<sort>/<t>')
@auth_required
def feeds_user(v=None, sort='hot', t='all'):
try: page = max(int(request.values.get("page", 1)), 1)
except: page = 1
ids, next_exists = frontlist(
sort=sort,
page=page,
t=t,
v=None,
)
posts = get_posts(ids)
domain = environ.get("DOMAIN").strip()
doc, tag, text = Doc().tagtext()
with tag("feed", ("xmlns:media","http://search.yahoo.com/mrss/"), xmlns="http://www.w3.org/2005/Atom",):
with tag("title", type="text"):
text(f"{sort} posts from {domain}")
doc.stag("link", href=SITE_FULL + request.full_path)
doc.stag("link", href=SITE_FULL)
for post in posts:
with tag("entry", ("xml:base", SITE_FULL + request.full_path)):
with tag("title", type="text"):
text(post.realtitle(None))
with tag("id"):
text(post.fullname)
if (post.edited_utc):
with tag("updated"):
text(datetime.utcfromtimestamp(post.edited_utc).isoformat())
with tag("published"):
text(datetime.utcfromtimestamp(post.created_utc).isoformat())
with tag("author"):
with tag("name"):
text(post.author_name)
with tag("uri"):
text(f'/@{post.author_name}')
doc.stag("link", href=post.permalink)
image_url = post.thumb_url or post.embed_url or post.url
doc.stag("media:thumbnail", url=image_url)
if len(post.body_html):
with tag("content", type="html"):
doc.cdata(f'''<img alt="{post.realtitle(None)}" loading="lazy" src={image_url}><br>{post.realbody(None)}''')
import html
from .front import frontlist
from datetime import datetime
from files.helpers.get import *
from yattag import Doc
from files.helpers.wrappers import *
from files.helpers.jinja2 import *
from files.__main__ import app
@app.get('/rss/<sort>/<t>')
@auth_required
def feeds_user(v=None, sort='hot', t='all'):
try: page = max(int(request.values.get("page", 1)), 1)
except: page = 1
ids, next_exists = frontlist(
sort=sort,
page=page,
t=t,
v=None,
)
posts = get_posts(ids)
domain = environ.get("DOMAIN").strip()
doc, tag, text = Doc().tagtext()
with tag("feed", ("xmlns:media","http://search.yahoo.com/mrss/"), xmlns="http://www.w3.org/2005/Atom",):
with tag("title", type="text"):
text(f"{sort} posts from {domain}")
doc.stag("link", href=SITE_FULL + request.full_path)
doc.stag("link", href=SITE_FULL)
for post in posts:
with tag("entry", ("xml:base", SITE_FULL + request.full_path)):
with tag("title", type="text"):
text(post.realtitle(None))
with tag("id"):
text(post.fullname)
if (post.edited_utc):
with tag("updated"):
text(datetime.utcfromtimestamp(post.edited_utc).isoformat())
with tag("published"):
text(datetime.utcfromtimestamp(post.created_utc).isoformat())
with tag("author"):
with tag("name"):
text(post.author_name)
with tag("uri"):
text(f'/@{post.author_name}')
doc.stag("link", href=post.permalink)
image_url = post.thumb_url or post.embed_url or post.url
doc.stag("media:thumbnail", url=image_url)
if len(post.body_html):
with tag("content", type="html"):
doc.cdata(f'''<img alt="{post.realtitle(None)}" loading="lazy" src={image_url}><br>{post.realbody(None)}''')
return Response( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+ doc.getvalue(), mimetype="application/xml")

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,24 @@
from flask import *
from os import environ
import requests
from files.helpers.wrappers import *
from files.__main__ import app
GIPHY_KEY = environ.get('GIPHY_KEY').rstrip()
@app.get("/giphy")
@app.get("/giphy<path>")
@auth_required
def giphy(v=None, path=None):
searchTerm = request.values.get("searchTerm", "").strip()
limit = int(request.values.get("limit", 48))
if searchTerm and limit:
url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit={limit}"
elif searchTerm and not limit:
url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit=48"
else:
url = f"https://api.giphy.com/v1/gifs?api_key={GIPHY_KEY}&limit=48"
return jsonify(requests.get(url, timeout=5).json())
from flask import *
from os import environ
import requests
from files.helpers.wrappers import *
from files.__main__ import app
GIPHY_KEY = environ.get('GIPHY_KEY').rstrip()
@app.get("/giphy")
@app.get("/giphy<path>")
@auth_required
def giphy(v=None, path=None):
searchTerm = request.values.get("searchTerm", "").strip()
limit = int(request.values.get("limit", 48))
if searchTerm and limit:
url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit={limit}"
elif searchTerm and not limit:
url = f"https://api.giphy.com/v1/gifs/search?q={searchTerm}&api_key={GIPHY_KEY}&limit=48"
else:
url = f"https://api.giphy.com/v1/gifs?api_key={GIPHY_KEY}&limit=48"
return jsonify(requests.get(url, timeout=5).json())

File diff suppressed because it is too large Load diff

View file

@ -1,281 +1,281 @@
from files.helpers.wrappers import *
from files.helpers.alerts import *
from files.helpers.get import *
from files.helpers.const import *
from files.classes import *
from flask import *
from files.__main__ import app, limiter
import sqlalchemy.exc
@app.get("/authorize")
@auth_required
def authorize_prompt(v):
client_id = request.values.get("client_id")
application = g.db.query(OauthApp).filter_by(client_id=client_id).one_or_none()
if not application: return {"oauth_error": "Invalid `client_id`"}, 401
return render_template("oauth.html", v=v, application=application)
@app.post("/authorize")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def authorize(v):
client_id = request.values.get("client_id")
application = g.db.query(OauthApp).filter_by(client_id=client_id).one_or_none()
if not application: return {"oauth_error": "Invalid `client_id`"}, 401
access_token = secrets.token_urlsafe(128)[:128]
try:
new_auth = ClientAuth(oauth_client = application.id, user_id = v.id, access_token=access_token)
g.db.add(new_auth)
g.db.commit()
except sqlalchemy.exc.IntegrityError:
g.db.rollback()
old_auth = g.db.query(ClientAuth).filter_by(oauth_client = application.id, user_id = v.id).one()
access_token = old_auth.access_token
return redirect(f"{application.redirect_uri}?token={access_token}")
@app.post("/api_keys")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@is_not_permabanned
def request_api_keys(v):
new_app = OauthApp(
app_name=request.values.get('name').replace('<','').replace('>',''),
redirect_uri=request.values.get('redirect_uri'),
author_id=v.id,
description=request.values.get("description")[:256]
)
g.db.add(new_app)
body = f"@{v.username} has requested API keys for `{request.values.get('name')}`. You can approve or deny the request [here](/admin/apps)."
body_html = sanitize(body)
new_comment = Comment(author_id=NOTIFICATIONS_ID,
parent_submission=None,
level=1,
body_html=body_html,
sentto=2,
distinguish_level=6
)
g.db.add(new_comment)
g.db.flush()
new_comment.top_comment_id = new_comment.id
for admin in g.db.query(User).filter(User.admin_level > 2).all():
notif = Notification(comment_id=new_comment.id, user_id=admin.id)
g.db.add(notif)
g.db.commit()
return redirect('/settings/apps')
@app.post("/delete_app/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def delete_oauth_app(v, aid):
aid = int(aid)
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if app.author_id != v.id: abort(403)
for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all():
g.db.delete(auth)
g.db.delete(app)
g.db.commit()
return redirect('/apps')
@app.post("/edit_app/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@is_not_permabanned
def edit_oauth_app(v, aid):
aid = int(aid)
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if app.author_id != v.id: abort(403)
app.redirect_uri = request.values.get('redirect_uri')
app.app_name = request.values.get('name')
app.description = request.values.get("description")[:256]
g.db.add(app)
g.db.commit()
return redirect('/settings/apps')
@app.post("/admin/app/approve/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(3)
def admin_app_approve(v, aid):
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
user = app.author
app.client_id = secrets.token_urlsafe(64)[:64]
g.db.add(app)
access_token = secrets.token_urlsafe(128)[:128]
new_auth = ClientAuth(
oauth_client = app.id,
user_id = user.id,
access_token=access_token
)
g.db.add(new_auth)
send_repeatable_notification(user.id, f"@{v.username} has approved your application `{app.app_name}`. Here's your access token: `{access_token}`\nPlease check the guide [here](/api) if you don't know what to do next.")
ma = ModAction(
kind="approve_app",
user_id=v.id,
target_user_id=user.id,
)
g.db.add(ma)
g.db.commit()
return {"message": "Application approved"}
@app.post("/admin/app/revoke/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(2)
def admin_app_revoke(v, aid):
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if app:
for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all(): g.db.delete(auth)
send_repeatable_notification(app.author.id, f"@{v.username} has revoked your application `{app.app_name}`.")
g.db.delete(app)
ma = ModAction(
kind="revoke_app",
user_id=v.id,
target_user_id=app.author.id,
)
g.db.add(ma)
g.db.commit()
return {"message": "App revoked"}
@app.post("/admin/app/reject/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(2)
def admin_app_reject(v, aid):
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if app:
for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all(): g.db.delete(auth)
send_repeatable_notification(app.author.id, f"@{v.username} has rejected your application `{app.app_name}`.")
g.db.delete(app)
ma = ModAction(
kind="reject_app",
user_id=v.id,
target_user_id=app.author.id,
)
g.db.add(ma)
g.db.commit()
return {"message": "App rejected"}
@app.get("/admin/app/<aid>")
@admin_level_required(2)
def admin_app_id(v, aid):
aid=aid
oauth = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
pids=oauth.idlist(page=int(request.values.get("page",1)))
next_exists=len(pids)==101
pids=pids[:100]
posts=get_posts(pids, v=v)
return render_template("admin/app.html",
v=v,
app=oauth,
listing=posts,
next_exists=next_exists
)
@app.get("/admin/app/<aid>/comments")
@admin_level_required(2)
def admin_app_id_comments(v, aid):
aid=aid
oauth = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
cids=oauth.comments_idlist(page=int(request.values.get("page",1)),
)
next_exists=len(cids)==101
cids=cids[:100]
comments=get_comments(cids, v=v)
return render_template("admin/app.html",
v=v,
app=oauth,
comments=comments,
next_exists=next_exists,
standalone=True
)
@app.get("/admin/apps")
@admin_level_required(2)
def admin_apps_list(v):
apps = g.db.query(OauthApp).order_by(OauthApp.id.desc()).all()
return render_template("admin/apps.html", v=v, apps=apps)
@app.post("/oauth/reroll/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def reroll_oauth_tokens(aid, v):
aid = aid
a = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if a.author_id != v.id: abort(403)
a.client_id = secrets.token_urlsafe(64)[:64]
g.db.add(a)
g.db.commit()
return {"message": "Client ID Rerolled", "id": a.client_id}
from files.helpers.wrappers import *
from files.helpers.alerts import *
from files.helpers.get import *
from files.helpers.const import *
from files.classes import *
from flask import *
from files.__main__ import app, limiter
import sqlalchemy.exc
@app.get("/authorize")
@auth_required
def authorize_prompt(v):
client_id = request.values.get("client_id")
application = g.db.query(OauthApp).filter_by(client_id=client_id).one_or_none()
if not application: return {"oauth_error": "Invalid `client_id`"}, 401
return render_template("oauth.html", v=v, application=application)
@app.post("/authorize")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def authorize(v):
client_id = request.values.get("client_id")
application = g.db.query(OauthApp).filter_by(client_id=client_id).one_or_none()
if not application: return {"oauth_error": "Invalid `client_id`"}, 401
access_token = secrets.token_urlsafe(128)[:128]
try:
new_auth = ClientAuth(oauth_client = application.id, user_id = v.id, access_token=access_token)
g.db.add(new_auth)
g.db.commit()
except sqlalchemy.exc.IntegrityError:
g.db.rollback()
old_auth = g.db.query(ClientAuth).filter_by(oauth_client = application.id, user_id = v.id).one()
access_token = old_auth.access_token
return redirect(f"{application.redirect_uri}?token={access_token}")
@app.post("/api_keys")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@is_not_permabanned
def request_api_keys(v):
new_app = OauthApp(
app_name=request.values.get('name').replace('<','').replace('>',''),
redirect_uri=request.values.get('redirect_uri'),
author_id=v.id,
description=request.values.get("description")[:256]
)
g.db.add(new_app)
body = f"@{v.username} has requested API keys for `{request.values.get('name')}`. You can approve or deny the request [here](/admin/apps)."
body_html = sanitize(body)
new_comment = Comment(author_id=NOTIFICATIONS_ID,
parent_submission=None,
level=1,
body_html=body_html,
sentto=2,
distinguish_level=6
)
g.db.add(new_comment)
g.db.flush()
new_comment.top_comment_id = new_comment.id
for admin in g.db.query(User).filter(User.admin_level > 2).all():
notif = Notification(comment_id=new_comment.id, user_id=admin.id)
g.db.add(notif)
g.db.commit()
return redirect('/settings/apps')
@app.post("/delete_app/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def delete_oauth_app(v, aid):
aid = int(aid)
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if app.author_id != v.id: abort(403)
for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all():
g.db.delete(auth)
g.db.delete(app)
g.db.commit()
return redirect('/apps')
@app.post("/edit_app/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@is_not_permabanned
def edit_oauth_app(v, aid):
aid = int(aid)
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if app.author_id != v.id: abort(403)
app.redirect_uri = request.values.get('redirect_uri')
app.app_name = request.values.get('name')
app.description = request.values.get("description")[:256]
g.db.add(app)
g.db.commit()
return redirect('/settings/apps')
@app.post("/admin/app/approve/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(3)
def admin_app_approve(v, aid):
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
user = app.author
app.client_id = secrets.token_urlsafe(64)[:64]
g.db.add(app)
access_token = secrets.token_urlsafe(128)[:128]
new_auth = ClientAuth(
oauth_client = app.id,
user_id = user.id,
access_token=access_token
)
g.db.add(new_auth)
send_repeatable_notification(user.id, f"@{v.username} has approved your application `{app.app_name}`. Here's your access token: `{access_token}`\nPlease check the guide [here](/api) if you don't know what to do next.")
ma = ModAction(
kind="approve_app",
user_id=v.id,
target_user_id=user.id,
)
g.db.add(ma)
g.db.commit()
return {"message": "Application approved"}
@app.post("/admin/app/revoke/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(2)
def admin_app_revoke(v, aid):
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if app:
for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all(): g.db.delete(auth)
send_repeatable_notification(app.author.id, f"@{v.username} has revoked your application `{app.app_name}`.")
g.db.delete(app)
ma = ModAction(
kind="revoke_app",
user_id=v.id,
target_user_id=app.author.id,
)
g.db.add(ma)
g.db.commit()
return {"message": "App revoked"}
@app.post("/admin/app/reject/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(2)
def admin_app_reject(v, aid):
app = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if app:
for auth in g.db.query(ClientAuth).filter_by(oauth_client=app.id).all(): g.db.delete(auth)
send_repeatable_notification(app.author.id, f"@{v.username} has rejected your application `{app.app_name}`.")
g.db.delete(app)
ma = ModAction(
kind="reject_app",
user_id=v.id,
target_user_id=app.author.id,
)
g.db.add(ma)
g.db.commit()
return {"message": "App rejected"}
@app.get("/admin/app/<aid>")
@admin_level_required(2)
def admin_app_id(v, aid):
aid=aid
oauth = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
pids=oauth.idlist(page=int(request.values.get("page",1)))
next_exists=len(pids)==101
pids=pids[:100]
posts=get_posts(pids, v=v)
return render_template("admin/app.html",
v=v,
app=oauth,
listing=posts,
next_exists=next_exists
)
@app.get("/admin/app/<aid>/comments")
@admin_level_required(2)
def admin_app_id_comments(v, aid):
aid=aid
oauth = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
cids=oauth.comments_idlist(page=int(request.values.get("page",1)),
)
next_exists=len(cids)==101
cids=cids[:100]
comments=get_comments(cids, v=v)
return render_template("admin/app.html",
v=v,
app=oauth,
comments=comments,
next_exists=next_exists,
standalone=True
)
@app.get("/admin/apps")
@admin_level_required(2)
def admin_apps_list(v):
apps = g.db.query(OauthApp).order_by(OauthApp.id.desc()).all()
return render_template("admin/apps.html", v=v, apps=apps)
@app.post("/oauth/reroll/<aid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def reroll_oauth_tokens(aid, v):
aid = aid
a = g.db.query(OauthApp).filter_by(id=aid).one_or_none()
if a.author_id != v.id: abort(403)
a.client_id = secrets.token_urlsafe(64)[:64]
g.db.add(a)
g.db.commit()
return {"message": "Client ID Rerolled", "id": a.client_id}

File diff suppressed because it is too large Load diff

View file

@ -1,138 +1,138 @@
from files.helpers.wrappers import *
from files.helpers.get import *
from flask import g
from files.__main__ import app, limiter
from os import path
from files.helpers.sanitize import filter_emojis_only
@app.post("/report/post/<pid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def api_flag_post(pid, v):
post = get_post(pid)
reason = request.values.get("reason", "").strip()
if blackjack and any(i in reason.lower() for i in blackjack.split()):
v.shadowbanned = 'AutoJanny'
send_repeatable_notification(CARP_ID, f"reports on {post.permalink}")
reason = reason[:100]
if not reason.startswith('!'):
existing = g.db.query(Flag.post_id).filter_by(user_id=v.id, post_id=post.id).one_or_none()
if existing: return "", 409
reason = filter_emojis_only(reason)
if len(reason) > 350: return {"error": "Too long."}
if reason.startswith('!') and v.admin_level > 1:
post.flair = reason[1:]
g.db.add(post)
ma=ModAction(
kind="flair_post",
user_id=v.id,
target_submission_id=post.id,
_note=f'"{post.flair}"'
)
g.db.add(ma)
elif reason.startswith('/h/') and v.admin_level > 2:
post.sub = reason[3:]
g.db.add(post)
ma=ModAction(
kind="move_hole",
user_id=v.id,
target_submission_id=post.id,
)
g.db.add(ma)
else:
flag = Flag(post_id=post.id, user_id=v.id, reason=reason)
g.db.add(flag)
g.db.commit()
return {"message": "Post reported!"}
@app.post("/report/comment/<cid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def api_flag_comment(cid, v):
comment = get_comment(cid)
existing = g.db.query(CommentFlag.comment_id).filter_by( user_id=v.id, comment_id=comment.id).one_or_none()
if existing: return "", 409
reason = request.values.get("reason", "").strip()
if blackjack and any(i in reason.lower() for i in blackjack.split()):
v.shadowbanned = 'AutoJanny'
send_repeatable_notification(CARP_ID, f"reports on {comment.permalink}")
reason = reason[:100]
reason = filter_emojis_only(reason)
if len(reason) > 350: return {"error": "Too long."}
flag = CommentFlag(comment_id=comment.id, user_id=v.id, reason=reason)
g.db.add(flag)
g.db.commit()
return {"message": "Comment reported!"}
@app.post('/del_report/post/<pid>/<uid>')
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(2)
def remove_report_post(v, pid, uid):
try:
pid = int(pid)
uid = int(uid)
except: abort(400)
report = g.db.query(Flag).filter_by(post_id=pid, user_id=uid).one()
g.db.delete(report)
ma=ModAction(
kind="delete_report",
user_id=v.id,
target_submission_id=pid
)
g.db.add(ma)
g.db.commit()
return {"message": "Report removed successfully!"}
@app.post('/del_report/comment/<cid>/<uid>')
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(2)
def remove_report_comment(v, cid, uid):
cid = int(cid)
uid = int(uid)
report = g.db.query(CommentFlag).filter_by(comment_id=cid, user_id=uid).one()
g.db.delete(report)
ma=ModAction(
kind="delete_report",
user_id=v.id,
target_comment_id=cid
)
g.db.add(ma)
g.db.commit()
from files.helpers.wrappers import *
from files.helpers.get import *
from flask import g
from files.__main__ import app, limiter
from os import path
from files.helpers.sanitize import filter_emojis_only
@app.post("/report/post/<pid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def api_flag_post(pid, v):
post = get_post(pid)
reason = request.values.get("reason", "").strip()
if blackjack and any(i in reason.lower() for i in blackjack.split()):
v.shadowbanned = 'AutoJanny'
send_repeatable_notification(CARP_ID, f"reports on {post.permalink}")
reason = reason[:100]
if not reason.startswith('!'):
existing = g.db.query(Flag.post_id).filter_by(user_id=v.id, post_id=post.id).one_or_none()
if existing: return "", 409
reason = filter_emojis_only(reason)
if len(reason) > 350: return {"error": "Too long."}
if reason.startswith('!') and v.admin_level > 1:
post.flair = reason[1:]
g.db.add(post)
ma=ModAction(
kind="flair_post",
user_id=v.id,
target_submission_id=post.id,
_note=f'"{post.flair}"'
)
g.db.add(ma)
elif reason.startswith('/h/') and v.admin_level > 2:
post.sub = reason[3:]
g.db.add(post)
ma=ModAction(
kind="move_hole",
user_id=v.id,
target_submission_id=post.id,
)
g.db.add(ma)
else:
flag = Flag(post_id=post.id, user_id=v.id, reason=reason)
g.db.add(flag)
g.db.commit()
return {"message": "Post reported!"}
@app.post("/report/comment/<cid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def api_flag_comment(cid, v):
comment = get_comment(cid)
existing = g.db.query(CommentFlag.comment_id).filter_by( user_id=v.id, comment_id=comment.id).one_or_none()
if existing: return "", 409
reason = request.values.get("reason", "").strip()
if blackjack and any(i in reason.lower() for i in blackjack.split()):
v.shadowbanned = 'AutoJanny'
send_repeatable_notification(CARP_ID, f"reports on {comment.permalink}")
reason = reason[:100]
reason = filter_emojis_only(reason)
if len(reason) > 350: return {"error": "Too long."}
flag = CommentFlag(comment_id=comment.id, user_id=v.id, reason=reason)
g.db.add(flag)
g.db.commit()
return {"message": "Comment reported!"}
@app.post('/del_report/post/<pid>/<uid>')
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(2)
def remove_report_post(v, pid, uid):
try:
pid = int(pid)
uid = int(uid)
except: abort(400)
report = g.db.query(Flag).filter_by(post_id=pid, user_id=uid).one()
g.db.delete(report)
ma=ModAction(
kind="delete_report",
user_id=v.id,
target_submission_id=pid
)
g.db.add(ma)
g.db.commit()
return {"message": "Report removed successfully!"}
@app.post('/del_report/comment/<cid>/<uid>')
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@admin_level_required(2)
def remove_report_comment(v, cid, uid):
cid = int(cid)
uid = int(uid)
report = g.db.query(CommentFlag).filter_by(comment_id=cid, user_id=uid).one()
g.db.delete(report)
ma=ModAction(
kind="delete_report",
user_id=v.id,
target_comment_id=cid
)
g.db.add(ma)
g.db.commit()
return {"message": "Report removed successfully!"}

View file

@ -1,290 +1,290 @@
from files.helpers.wrappers import *
import re
from sqlalchemy import *
from flask import *
from files.__main__ import app
valid_params=[
'author',
'domain',
'over18'
]
def searchparse(text):
criteria = {x[0]:x[1] for x in query_regex.findall(text)}
for x in criteria:
if x in valid_params:
text = text.replace(f"{x}:{criteria[x]}", "")
text=text.strip()
if text:
criteria['q']=text
return criteria
@app.get("/search/posts")
@auth_required
def searchposts(v):
query = request.values.get("q", '').strip()
page = max(1, int(request.values.get("page", 1)))
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
criteria=searchparse(query)
posts = g.db.query(Submission.id)
if not v.paid_dues: posts = posts.filter_by(club=False)
if v.admin_level < 2:
posts = posts.filter(Submission.deleted_utc == 0, Submission.is_banned == False, Submission.private == False, Submission.author_id.notin_(v.userblocks))
if 'author' in criteria:
posts = posts.filter(Submission.ghost == False)
author = get_user(criteria['author'])
if not author: return {"error": "User not found"}
if author.is_private and author.id != v.id and v.admin_level < 2 and not v.eye:
if request.headers.get("Authorization"):
return {"error": f"@{author.username}'s profile is private; You can't use the 'author' syntax on them"}
return render_template("search.html",
v=v,
query=query,
total=0,
page=page,
listing=[],
sort=sort,
t=t,
next_exists=False,
domain=None,
domain_obj=None,
error=f"@{author.username}'s profile is private; You can't use the 'author' syntax on them."
)
else: posts = posts.filter(Submission.author_id == author.id)
if 'q' in criteria:
words=criteria['q'].split()
words = criteria['q'].replace('\\', '').replace('_', '\_').replace('%', '\%').strip().split()
words=[Submission.title.ilike('%'+x+'%') for x in words]
posts=posts.filter(*words)
if 'over18' in criteria: posts = posts.filter(Submission.over_18==True)
if 'domain' in criteria:
domain=criteria['domain']
domain = domain.replace('\\', '').replace('_', '\_').replace('%', '').strip()
posts=posts.filter(
or_(
Submission.url.ilike("https://"+domain+'/%'),
Submission.url.ilike("https://"+domain+'/%'),
Submission.url.ilike("https://"+domain),
Submission.url.ilike("https://"+domain),
Submission.url.ilike("https://www."+domain+'/%'),
Submission.url.ilike("https://www."+domain+'/%'),
Submission.url.ilike("https://www."+domain),
Submission.url.ilike("https://www."+domain),
Submission.url.ilike("https://old." + domain + '/%'),
Submission.url.ilike("https://old." + domain + '/%'),
Submission.url.ilike("https://old." + domain),
Submission.url.ilike("https://old." + domain)
)
)
if t:
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
posts = posts.filter(Submission.created_utc >= cutoff)
if sort == "new":
posts = posts.order_by(Submission.created_utc.desc())
elif sort == "old":
posts = posts.order_by(Submission.created_utc)
elif sort == "controversial":
posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc())
elif sort == "top":
posts = posts.order_by(Submission.downvotes - Submission.upvotes)
elif sort == "bottom":
posts = posts.order_by(Submission.upvotes - Submission.downvotes)
elif sort == "comments":
posts = posts.order_by(Submission.comment_count.desc())
total = posts.count()
posts = posts.offset(25 * (page - 1)).limit(26).all()
ids = [x[0] for x in posts]
next_exists = (len(ids) > 25)
ids = ids[:25]
posts = get_posts(ids, v=v)
if request.headers.get("Authorization"): return {"total":total, "data":[x.json for x in posts]}
return render_template("search.html",
v=v,
query=query,
total=total,
page=page,
listing=posts,
sort=sort,
t=t,
next_exists=next_exists
)
@app.get("/search/comments")
@auth_required
def searchcomments(v):
query = request.values.get("q", '').strip()
try: page = max(1, int(request.values.get("page", 1)))
except: page = 1
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
criteria = searchparse(query)
comments = g.db.query(Comment.id).filter(Comment.parent_submission != None)
if 'author' in criteria:
comments = comments.filter(Comment.ghost == False)
author = get_user(criteria['author'])
if not author: return {"error": "User not found"}
if author.is_private and author.id != v.id and v.admin_level < 2 and not v.eye:
if request.headers.get("Authorization"):
return {"error": f"@{author.username}'s profile is private; You can't use the 'author' syntax on them"}
return render_template("search_comments.html", v=v, query=query, total=0, page=page, comments=[], sort=sort, t=t, next_exists=False, error=f"@{author.username}'s profile is private; You can't use the 'author' syntax on them.")
else: comments = comments.filter(Comment.author_id == author.id)
if 'q' in criteria:
words = criteria['q'].replace('\\', '').replace('_', '\_').replace('%', '\%').strip().split()
words = [Comment.body.ilike('%'+x+'%') for x in words]
comments = comments.filter(*words)
if 'over18' in criteria: comments = comments.filter(Comment.over_18 == True)
if t:
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
comments = comments.filter(Comment.created_utc >= cutoff)
if v.admin_level < 2:
private = [x[0] for x in g.db.query(Submission.id).filter(Submission.private == True).all()]
comments = comments.filter(Comment.author_id.notin_(v.userblocks), Comment.is_banned==False, Comment.deleted_utc == 0, Comment.parent_submission.notin_(private))
if not v.paid_dues:
club = [x[0] for x in g.db.query(Submission.id).filter(Submission.club == True).all()]
comments = comments.filter(Comment.parent_submission.notin_(club))
if sort == "new":
comments = comments.order_by(Comment.created_utc.desc())
elif sort == "old":
comments = comments.order_by(Comment.created_utc)
elif sort == "controversial":
comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc())
elif sort == "top":
comments = comments.order_by(Comment.downvotes - Comment.upvotes)
elif sort == "bottom":
comments = comments.order_by(Comment.upvotes - Comment.downvotes)
total = comments.count()
comments = comments.offset(25 * (page - 1)).limit(26).all()
ids = [x[0] for x in comments]
next_exists = (len(ids) > 25)
ids = ids[:25]
comments = get_comments(ids, v=v)
if request.headers.get("Authorization"): return {"total":total, "data":[x.json for x in comments]}
return render_template("search_comments.html", v=v, query=query, total=total, page=page, comments=comments, sort=sort, t=t, next_exists=next_exists, standalone=True)
@app.get("/search/users")
@auth_required
def searchusers(v):
query = request.values.get("q", '').strip()
page = max(1, int(request.values.get("page", 1)))
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
term=query.lstrip('@')
term = term.replace('\\','').replace('_','\_').replace('%','')
users=g.db.query(User).filter(User.username.ilike(f'%{term}%'))
users=users.order_by(User.username.ilike(term).desc(), User.stored_subscriber_count.desc())
total=users.count()
users=[x for x in users.offset(25 * (page-1)).limit(26)]
next_exists=(len(users)>25)
users=users[:25]
if request.headers.get("Authorization"): return {"data": [x.json for x in users]}
from files.helpers.wrappers import *
import re
from sqlalchemy import *
from flask import *
from files.__main__ import app
valid_params=[
'author',
'domain',
'over18'
]
def searchparse(text):
criteria = {x[0]:x[1] for x in query_regex.findall(text)}
for x in criteria:
if x in valid_params:
text = text.replace(f"{x}:{criteria[x]}", "")
text=text.strip()
if text:
criteria['q']=text
return criteria
@app.get("/search/posts")
@auth_required
def searchposts(v):
query = request.values.get("q", '').strip()
page = max(1, int(request.values.get("page", 1)))
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
criteria=searchparse(query)
posts = g.db.query(Submission.id)
if not v.paid_dues: posts = posts.filter_by(club=False)
if v.admin_level < 2:
posts = posts.filter(Submission.deleted_utc == 0, Submission.is_banned == False, Submission.private == False, Submission.author_id.notin_(v.userblocks))
if 'author' in criteria:
posts = posts.filter(Submission.ghost == False)
author = get_user(criteria['author'])
if not author: return {"error": "User not found"}
if author.is_private and author.id != v.id and v.admin_level < 2 and not v.eye:
if request.headers.get("Authorization"):
return {"error": f"@{author.username}'s profile is private; You can't use the 'author' syntax on them"}
return render_template("search.html",
v=v,
query=query,
total=0,
page=page,
listing=[],
sort=sort,
t=t,
next_exists=False,
domain=None,
domain_obj=None,
error=f"@{author.username}'s profile is private; You can't use the 'author' syntax on them."
)
else: posts = posts.filter(Submission.author_id == author.id)
if 'q' in criteria:
words=criteria['q'].split()
words = criteria['q'].replace('\\', '').replace('_', '\_').replace('%', '\%').strip().split()
words=[Submission.title.ilike('%'+x+'%') for x in words]
posts=posts.filter(*words)
if 'over18' in criteria: posts = posts.filter(Submission.over_18==True)
if 'domain' in criteria:
domain=criteria['domain']
domain = domain.replace('\\', '').replace('_', '\_').replace('%', '').strip()
posts=posts.filter(
or_(
Submission.url.ilike("https://"+domain+'/%'),
Submission.url.ilike("https://"+domain+'/%'),
Submission.url.ilike("https://"+domain),
Submission.url.ilike("https://"+domain),
Submission.url.ilike("https://www."+domain+'/%'),
Submission.url.ilike("https://www."+domain+'/%'),
Submission.url.ilike("https://www."+domain),
Submission.url.ilike("https://www."+domain),
Submission.url.ilike("https://old." + domain + '/%'),
Submission.url.ilike("https://old." + domain + '/%'),
Submission.url.ilike("https://old." + domain),
Submission.url.ilike("https://old." + domain)
)
)
if t:
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
posts = posts.filter(Submission.created_utc >= cutoff)
if sort == "new":
posts = posts.order_by(Submission.created_utc.desc())
elif sort == "old":
posts = posts.order_by(Submission.created_utc)
elif sort == "controversial":
posts = posts.order_by((Submission.upvotes+1)/(Submission.downvotes+1) + (Submission.downvotes+1)/(Submission.upvotes+1), Submission.downvotes.desc())
elif sort == "top":
posts = posts.order_by(Submission.downvotes - Submission.upvotes)
elif sort == "bottom":
posts = posts.order_by(Submission.upvotes - Submission.downvotes)
elif sort == "comments":
posts = posts.order_by(Submission.comment_count.desc())
total = posts.count()
posts = posts.offset(25 * (page - 1)).limit(26).all()
ids = [x[0] for x in posts]
next_exists = (len(ids) > 25)
ids = ids[:25]
posts = get_posts(ids, v=v)
if request.headers.get("Authorization"): return {"total":total, "data":[x.json for x in posts]}
return render_template("search.html",
v=v,
query=query,
total=total,
page=page,
listing=posts,
sort=sort,
t=t,
next_exists=next_exists
)
@app.get("/search/comments")
@auth_required
def searchcomments(v):
query = request.values.get("q", '').strip()
try: page = max(1, int(request.values.get("page", 1)))
except: page = 1
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
criteria = searchparse(query)
comments = g.db.query(Comment.id).filter(Comment.parent_submission != None)
if 'author' in criteria:
comments = comments.filter(Comment.ghost == False)
author = get_user(criteria['author'])
if not author: return {"error": "User not found"}
if author.is_private and author.id != v.id and v.admin_level < 2 and not v.eye:
if request.headers.get("Authorization"):
return {"error": f"@{author.username}'s profile is private; You can't use the 'author' syntax on them"}
return render_template("search_comments.html", v=v, query=query, total=0, page=page, comments=[], sort=sort, t=t, next_exists=False, error=f"@{author.username}'s profile is private; You can't use the 'author' syntax on them.")
else: comments = comments.filter(Comment.author_id == author.id)
if 'q' in criteria:
words = criteria['q'].replace('\\', '').replace('_', '\_').replace('%', '\%').strip().split()
words = [Comment.body.ilike('%'+x+'%') for x in words]
comments = comments.filter(*words)
if 'over18' in criteria: comments = comments.filter(Comment.over_18 == True)
if t:
now = int(time.time())
if t == 'hour':
cutoff = now - 3600
elif t == 'day':
cutoff = now - 86400
elif t == 'week':
cutoff = now - 604800
elif t == 'month':
cutoff = now - 2592000
elif t == 'year':
cutoff = now - 31536000
else:
cutoff = 0
comments = comments.filter(Comment.created_utc >= cutoff)
if v.admin_level < 2:
private = [x[0] for x in g.db.query(Submission.id).filter(Submission.private == True).all()]
comments = comments.filter(Comment.author_id.notin_(v.userblocks), Comment.is_banned==False, Comment.deleted_utc == 0, Comment.parent_submission.notin_(private))
if not v.paid_dues:
club = [x[0] for x in g.db.query(Submission.id).filter(Submission.club == True).all()]
comments = comments.filter(Comment.parent_submission.notin_(club))
if sort == "new":
comments = comments.order_by(Comment.created_utc.desc())
elif sort == "old":
comments = comments.order_by(Comment.created_utc)
elif sort == "controversial":
comments = comments.order_by((Comment.upvotes+1)/(Comment.downvotes+1) + (Comment.downvotes+1)/(Comment.upvotes+1), Comment.downvotes.desc())
elif sort == "top":
comments = comments.order_by(Comment.downvotes - Comment.upvotes)
elif sort == "bottom":
comments = comments.order_by(Comment.upvotes - Comment.downvotes)
total = comments.count()
comments = comments.offset(25 * (page - 1)).limit(26).all()
ids = [x[0] for x in comments]
next_exists = (len(ids) > 25)
ids = ids[:25]
comments = get_comments(ids, v=v)
if request.headers.get("Authorization"): return {"total":total, "data":[x.json for x in comments]}
return render_template("search_comments.html", v=v, query=query, total=total, page=page, comments=comments, sort=sort, t=t, next_exists=next_exists, standalone=True)
@app.get("/search/users")
@auth_required
def searchusers(v):
query = request.values.get("q", '').strip()
page = max(1, int(request.values.get("page", 1)))
sort = request.values.get("sort", "new").lower()
t = request.values.get('t', 'all').lower()
term=query.lstrip('@')
term = term.replace('\\','').replace('_','\_').replace('%','')
users=g.db.query(User).filter(User.username.ilike(f'%{term}%'))
users=users.order_by(User.username.ilike(term).desc(), User.stored_subscriber_count.desc())
total=users.count()
users=[x for x in users.offset(25 * (page-1)).limit(26)]
next_exists=(len(users)>25)
users=users[:25]
if request.headers.get("Authorization"): return {"data": [x.json for x in users]}
return render_template("search_users.html", v=v, query=query, total=total, page=page, users=users, sort=sort, t=t, next_exists=next_exists)

File diff suppressed because it is too large Load diff

View file

@ -1,464 +1,464 @@
from files.mail import *
from files.__main__ import app, limiter, mail
from files.helpers.alerts import *
from files.helpers.const import *
from files.classes.award import AWARDS
from sqlalchemy import func
from os import path
import calendar
import matplotlib.pyplot as plt
from files.classes.mod_logs import ACTIONTYPES, ACTIONTYPES2
from files.classes.badges import BadgeDef
@app.get("/r/drama/comments/<id>/<title>")
@app.get("/r/Drama/comments/<id>/<title>")
def rdrama(id, title):
id = ''.join(f'{x}/' for x in id)
return redirect(f'/archives/drama/comments/{id}{title}.html')
@app.get('/logged_out/')
@app.get('/logged_out/<path:old>')
def logged_out(old = ""):
# Remove trailing question mark from request.full_path which flask adds if there are no query parameters
redirect_url = request.full_path.replace("/logged_out", "", 1)
if redirect_url.endswith("?"):
redirect_url = redirect_url[:-1]
# Handle cases like /logged_out?asdf by adding a slash to the beginning
if not redirect_url.startswith('/'):
redirect_url = f"/{redirect_url}"
# Prevent redirect loop caused by visiting /logged_out/logged_out/logged_out/etc...
if redirect_url.startswith('/logged_out'):
abort(400)
return redirect(redirect_url)
@app.get("/marseys")
@auth_required
def marseys(v):
marseys = g.db.query(Marsey).order_by(Marsey.count.desc())
return render_template("marseys.html", v=v, marseys=marseys)
@app.get("/marsey_list")
@cache.memoize(timeout=600, make_name=make_name)
def marsey_list():
marseys = [f"{x.name} : {x.tags}" for x in g.db.query(Marsey).order_by(Marsey.count.desc())]
return str(marseys).replace("'",'"')
@app.get('/sidebar')
@auth_desired
def sidebar(v):
return render_template('sidebar.html', v=v)
@app.get('/rules')
@auth_desired
def rules(v):
return render_template('rules.html', v=v)
@app.get("/stats")
@auth_required
@cache.memoize(timeout=86400, make_name=make_name)
def participation_stats(v):
day = int(time.time()) - 86400
week = int(time.time()) - 604800
posters = g.db.query(Submission.author_id).distinct(Submission.author_id).filter(Submission.created_utc > week).all()
commenters = g.db.query(Comment.author_id).distinct(Comment.author_id).filter(Comment.created_utc > week).all()
voters = g.db.query(Vote.user_id).distinct(Vote.user_id).filter(Vote.created_utc > week).all()
commentvoters = g.db.query(CommentVote.user_id).distinct(CommentVote.user_id).filter(CommentVote.created_utc > week).all()
active_users = set(posters) | set(commenters) | set(voters) | set(commentvoters)
stats = {"marseys": g.db.query(Marsey.name).count(),
"users": g.db.query(User.id).count(),
"private users": g.db.query(User.id).filter_by(is_private=True).count(),
"banned users": g.db.query(User.id).filter(User.is_banned > 0).count(),
"verified email users": g.db.query(User.id).filter_by(is_activated=True).count(),
"coins in circulation": g.db.query(func.sum(User.coins)).scalar(),
"total shop sales": g.db.query(func.sum(User.coins_spent)).scalar(),
"signups last 24h": g.db.query(User.id).filter(User.created_utc > day).count(),
"total posts": g.db.query(Submission.id).count(),
"posting users": g.db.query(Submission.author_id).distinct().count(),
"listed posts": g.db.query(Submission.id).filter_by(is_banned=False).filter(Submission.deleted_utc == 0).count(),
"removed posts (by admins)": g.db.query(Submission.id).filter_by(is_banned=True).count(),
"deleted posts (by author)": g.db.query(Submission.id).filter(Submission.deleted_utc > 0).count(),
"posts last 24h": g.db.query(Submission.id).filter(Submission.created_utc > day).count(),
"total comments": g.db.query(Comment.id).filter(Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(),
"commenting users": g.db.query(Comment.author_id).distinct().count(),
"removed comments (by admins)": g.db.query(Comment.id).filter_by(is_banned=True).count(),
"deleted comments (by author)": g.db.query(Comment.id).filter(Comment.deleted_utc > 0).count(),
"comments last_24h": g.db.query(Comment.id).filter(Comment.created_utc > day, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(),
"post votes": g.db.query(Vote.submission_id).count(),
"post voting users": g.db.query(Vote.user_id).distinct().count(),
"comment votes": g.db.query(CommentVote.comment_id).count(),
"comment voting users": g.db.query(CommentVote.user_id).distinct().count(),
"total upvotes": g.db.query(Vote.submission_id).filter_by(vote_type=1).count() + g.db.query(CommentVote.comment_id).filter_by(vote_type=1).count(),
"total downvotes": g.db.query(Vote.submission_id).filter_by(vote_type=-1).count() + g.db.query(CommentVote.comment_id).filter_by(vote_type=-1).count(),
"total awards": g.db.query(AwardRelationship.id).count(),
"awards given": g.db.query(AwardRelationship.id).filter(or_(AwardRelationship.submission_id != None, AwardRelationship.comment_id != None)).count(),
"users who posted, commented, or voted in the past 7 days": len(active_users),
}
g.db.commit()
return render_template("admin/content_stats.html", v=v, title="Content Statistics", data=stats)
@app.get("/chart")
def chart():
return redirect('/weekly_chart')
@app.get("/weekly_chart")
@auth_required
def weekly_chart(v):
file = cached_chart(kind="weekly", site=SITE)
f = send_file(file)
return f
@app.get("/daily_chart")
@auth_required
def daily_chart(v):
file = cached_chart(kind="daily", site=SITE)
f = send_file(file)
return f
@cache.memoize(timeout=86400)
def cached_chart(kind, site):
now = time.gmtime()
midnight_this_morning = time.struct_time((now.tm_year,
now.tm_mon,
now.tm_mday,
0,
0,
0,
now.tm_wday,
now.tm_yday,
0)
)
today_cutoff = calendar.timegm(midnight_this_morning)
if kind == "daily": day_cutoffs = [today_cutoff - 86400 * i for i in range(47)][1:]
else: day_cutoffs = [today_cutoff - 86400 * 7 * i for i in range(47)][1:]
day_cutoffs.insert(0, calendar.timegm(now))
daily_times = [time.strftime("%d/%m", time.gmtime(day_cutoffs[i + 1])) for i in range(len(day_cutoffs) - 1)][::-1]
daily_signups = [g.db.query(User.id).filter(User.created_utc < day_cutoffs[i], User.created_utc > day_cutoffs[i + 1]).count() for i in range(len(day_cutoffs) - 1)][::-1]
post_stats = [g.db.query(Submission.id).filter(Submission.created_utc < day_cutoffs[i], Submission.created_utc > day_cutoffs[i + 1], Submission.is_banned == False).count() for i in range(len(day_cutoffs) - 1)][::-1]
comment_stats = [g.db.query(Comment.id).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count() for i in range(len(day_cutoffs) - 1)][::-1]
plt.rcParams["figure.figsize"] = (30, 20)
signup_chart = plt.subplot2grid((30, 20), (0, 0), rowspan=6, colspan=30)
posts_chart = plt.subplot2grid((30, 20), (10, 0), rowspan=6, colspan=30)
comments_chart = plt.subplot2grid((30, 20), (20, 0), rowspan=6, colspan=30)
signup_chart.grid(), posts_chart.grid(), comments_chart.grid()
signup_chart.plot(
daily_times,
daily_signups,
color='red')
posts_chart.plot(
daily_times,
post_stats,
color='blue')
comments_chart.plot(
daily_times,
comment_stats,
color='purple')
signup_chart.set_ylim(ymin=0)
posts_chart.set_ylim(ymin=0)
comments_chart.set_ylim(ymin=0)
signup_chart.set_ylabel("Signups")
posts_chart.set_ylabel("Posts")
comments_chart.set_ylabel("Comments")
comments_chart.set_xlabel("Time (UTC)")
signup_chart.legend(loc='upper left', frameon=True)
posts_chart.legend(loc='upper left', frameon=True)
comments_chart.legend(loc='upper left', frameon=True)
file = f"/{SITE}_{kind}.png"
plt.savefig(file)
plt.clf()
return file
@app.get("/patrons")
@app.get("/paypigs")
@admin_level_required(3)
def patrons(v):
users = g.db.query(User).filter(User.patron > 0).order_by(User.patron.desc(), User.id).all()
return render_template("patrons.html", v=v, users=users)
@app.get("/admins")
@app.get("/badmins")
@auth_required
def admins(v):
if v and v.admin_level > 2:
admins = g.db.query(User).filter(User.admin_level>1).order_by(User.truecoins.desc()).all()
admins += g.db.query(User).filter(User.admin_level==1).order_by(User.truecoins.desc()).all()
else: admins = g.db.query(User).filter(User.admin_level>0).order_by(User.truecoins.desc()).all()
return render_template("admins.html", v=v, admins=admins)
@app.get("/log")
@app.get("/modlog")
@auth_required
def log(v):
try: page = max(int(request.values.get("page", 1)), 1)
except: page = 1
admin = request.values.get("admin")
if admin: admin_id = get_id(admin)
else: admin_id = 0
kind = request.values.get("kind")
if v and v.admin_level > 1: types = ACTIONTYPES
else: types = ACTIONTYPES2
if kind not in types: kind = None
actions = g.db.query(ModAction)
if not (v and v.admin_level > 1):
actions = actions.filter(ModAction.kind.notin_(["shadowban","unshadowban","flair_post","edit_post"]))
if admin_id:
actions = actions.filter_by(user_id=admin_id)
kinds = set([x.kind for x in actions])
types2 = {}
for k,val in types.items():
if k in kinds: types2[k] = val
types = types2
if kind: actions = actions.filter_by(kind=kind)
actions = actions.order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all()
next_exists=len(actions)>25
actions=actions[:25]
admins = [x[0] for x in g.db.query(User.username).filter(User.admin_level > 1).order_by(User.username).all()]
return render_template("log.html", v=v, admins=admins, types=types, admin=admin, type=kind, actions=actions, next_exists=next_exists, page=page)
@app.get("/log/<id>")
@auth_required
def log_item(id, v):
try: id = int(id)
except: abort(404)
action=g.db.query(ModAction).filter_by(id=id).one_or_none()
if not action: abort(404)
admins = [x[0] for x in g.db.query(User.username).filter(User.admin_level > 1).all()]
if v and v.admin_level > 1: types = ACTIONTYPES
else: types = ACTIONTYPES2
return render_template("log.html", v=v, actions=[action], next_exists=False, page=1, action=action, admins=admins, types=types)
@app.get("/api")
@auth_required
def api(v):
return render_template("api.html", v=v)
@app.get("/contact")
@app.get("/press")
@app.get("/media")
@auth_required
def contact(v):
return render_template("contact.html", v=v)
@app.post("/send_admin")
@limiter.limit("1/second;2/minute;6/hour;10/day")
@auth_required
def submit_contact(v):
body = request.values.get("message")
if not body: abort(400)
body = f'This message has been sent automatically to all admins via [/contact](/contact)\n\nMessage:\n\n' + body
body_html = sanitize(body)
if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1":
file=request.files["file"]
if file.content_type.startswith('image/'):
name = f'/images/{time.time()}'.replace('.','') + '.webp'
file.save(name)
url = process_image(name)
body_html += f'<img data-bs-target="#expandImageModal" data-bs-toggle="modal" onclick="expandDesktopImage(this.src)" class="img" src="{url}" loading="lazy">'
elif file.content_type.startswith('video/'):
file.save("video.mp4")
with open("video.mp4", 'rb') as f:
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
except requests.Timeout: return {"error": "Video upload timed out, please try again!"}
try: url = req['link']
except:
error = req['error']
if error == 'File exceeds max duration': error += ' (60 seconds)'
return {"error": error}, 400
if url.endswith('.'): url += 'mp4'
body_html += f"<p>{url}</p>"
else: return {"error": "Image/Video files only"}, 400
new_comment = Comment(author_id=v.id,
parent_submission=None,
level=1,
body_html=body_html,
sentto=2
)
g.db.add(new_comment)
g.db.flush()
new_comment.top_comment_id = new_comment.id
for admin in g.db.query(User).filter(User.admin_level > 2).all():
notif = Notification(comment_id=new_comment.id, user_id=admin.id)
g.db.add(notif)
g.db.commit()
return render_template("contact.html", v=v, msg="Your message has been sent.")
@app.get('/archives')
def archivesindex():
return redirect("/archives/index.html")
@app.get('/archives/<path:path>')
def archives(path):
resp = make_response(send_from_directory('/archives', path))
if request.path.endswith('.css'): resp.headers.add("Content-Type", "text/css")
return resp
@app.get('/e/<emoji>')
@limiter.exempt
def emoji(emoji):
if not emoji.endswith('.webp'): abort(404)
resp = make_response(send_from_directory('assets/images/emojis', emoji))
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=3153600")
resp.headers.remove("Content-Type")
resp.headers.add("Content-Type", "image/webp")
return resp
@app.get('/assets/<path:path>')
@app.get('/static/assets/<path:path>')
@limiter.exempt
def static_service(path):
resp = make_response(send_from_directory('assets', path))
if request.path.endswith('.webp') or request.path.endswith('.gif') or request.path.endswith('.ttf') or request.path.endswith('.woff2'):
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=3153600")
if request.path.endswith('.webp'):
resp.headers.remove("Content-Type")
resp.headers.add("Content-Type", "image/webp")
return resp
@app.get('/images/<path>')
@app.get('/hostedimages/<path>')
@app.get("/static/images/<path>")
@limiter.exempt
def images(path):
resp = make_response(send_from_directory('/images', path.replace('.WEBP','.webp')))
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=3153600")
if request.path.endswith('.webp'):
resp.headers.remove("Content-Type")
resp.headers.add("Content-Type", "image/webp")
return resp
@app.get("/robots.txt")
def robots_txt():
try: f = send_file("assets/robots.txt")
except:
print('/robots.txt', flush=True)
abort(404)
return f
@app.get("/badges")
@auth_required
@cache.memoize(timeout=3600, make_name=make_name)
def badges(v):
badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all()
counts_raw = g.db.query(Badge.badge_id, func.count()).group_by(Badge.badge_id).all()
users = g.db.query(User.id).count()
counts = {}
for c in counts_raw:
counts[c[0]] = (c[1], float(c[1]) * 100 / max(users, 1))
return render_template("badges.html", v=v, badges=badges, counts=counts)
@app.get("/blocks")
@auth_required
def blocks(v):
blocks=g.db.query(UserBlock).all()
users = []
targets = []
for x in blocks:
users.append(get_account(x.user_id))
targets.append(get_account(x.target_id))
return render_template("blocks.html", v=v, users=users, targets=targets)
@app.get("/banned")
@auth_required
def banned(v):
users = [x for x in g.db.query(User).filter(User.is_banned > 0, User.unban_utc == 0).all()]
return render_template("banned.html", v=v, users=users)
@app.get("/formatting")
@auth_required
def formatting(v):
return render_template("formatting.html", v=v)
@app.get("/service-worker.js")
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
)
@app.get("/.well-known/assetlinks.json")
def googleplayapp():
with open("files/assets/assetlinks.json", "r") as f:
return Response(f.read(), mimetype='application/json')
@app.post("/dismiss_mobile_tip")
def dismiss_mobile_tip():
session["tooltip_last_dismissed"] = int(time.time())
return "", 204
from files.mail import *
from files.__main__ import app, limiter, mail
from files.helpers.alerts import *
from files.helpers.const import *
from files.classes.award import AWARDS
from sqlalchemy import func
from os import path
import calendar
import matplotlib.pyplot as plt
from files.classes.mod_logs import ACTIONTYPES, ACTIONTYPES2
from files.classes.badges import BadgeDef
@app.get("/r/drama/comments/<id>/<title>")
@app.get("/r/Drama/comments/<id>/<title>")
def rdrama(id, title):
id = ''.join(f'{x}/' for x in id)
return redirect(f'/archives/drama/comments/{id}{title}.html')
@app.get('/logged_out/')
@app.get('/logged_out/<path:old>')
def logged_out(old = ""):
# Remove trailing question mark from request.full_path which flask adds if there are no query parameters
redirect_url = request.full_path.replace("/logged_out", "", 1)
if redirect_url.endswith("?"):
redirect_url = redirect_url[:-1]
# Handle cases like /logged_out?asdf by adding a slash to the beginning
if not redirect_url.startswith('/'):
redirect_url = f"/{redirect_url}"
# Prevent redirect loop caused by visiting /logged_out/logged_out/logged_out/etc...
if redirect_url.startswith('/logged_out'):
abort(400)
return redirect(redirect_url)
@app.get("/marseys")
@auth_required
def marseys(v):
marseys = g.db.query(Marsey).order_by(Marsey.count.desc())
return render_template("marseys.html", v=v, marseys=marseys)
@app.get("/marsey_list")
@cache.memoize(timeout=600, make_name=make_name)
def marsey_list():
marseys = [f"{x.name} : {x.tags}" for x in g.db.query(Marsey).order_by(Marsey.count.desc())]
return str(marseys).replace("'",'"')
@app.get('/sidebar')
@auth_desired
def sidebar(v):
return render_template('sidebar.html', v=v)
@app.get('/rules')
@auth_desired
def rules(v):
return render_template('rules.html', v=v)
@app.get("/stats")
@auth_required
@cache.memoize(timeout=86400, make_name=make_name)
def participation_stats(v):
day = int(time.time()) - 86400
week = int(time.time()) - 604800
posters = g.db.query(Submission.author_id).distinct(Submission.author_id).filter(Submission.created_utc > week).all()
commenters = g.db.query(Comment.author_id).distinct(Comment.author_id).filter(Comment.created_utc > week).all()
voters = g.db.query(Vote.user_id).distinct(Vote.user_id).filter(Vote.created_utc > week).all()
commentvoters = g.db.query(CommentVote.user_id).distinct(CommentVote.user_id).filter(CommentVote.created_utc > week).all()
active_users = set(posters) | set(commenters) | set(voters) | set(commentvoters)
stats = {"marseys": g.db.query(Marsey.name).count(),
"users": g.db.query(User.id).count(),
"private users": g.db.query(User.id).filter_by(is_private=True).count(),
"banned users": g.db.query(User.id).filter(User.is_banned > 0).count(),
"verified email users": g.db.query(User.id).filter_by(is_activated=True).count(),
"coins in circulation": g.db.query(func.sum(User.coins)).scalar(),
"total shop sales": g.db.query(func.sum(User.coins_spent)).scalar(),
"signups last 24h": g.db.query(User.id).filter(User.created_utc > day).count(),
"total posts": g.db.query(Submission.id).count(),
"posting users": g.db.query(Submission.author_id).distinct().count(),
"listed posts": g.db.query(Submission.id).filter_by(is_banned=False).filter(Submission.deleted_utc == 0).count(),
"removed posts (by admins)": g.db.query(Submission.id).filter_by(is_banned=True).count(),
"deleted posts (by author)": g.db.query(Submission.id).filter(Submission.deleted_utc > 0).count(),
"posts last 24h": g.db.query(Submission.id).filter(Submission.created_utc > day).count(),
"total comments": g.db.query(Comment.id).filter(Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(),
"commenting users": g.db.query(Comment.author_id).distinct().count(),
"removed comments (by admins)": g.db.query(Comment.id).filter_by(is_banned=True).count(),
"deleted comments (by author)": g.db.query(Comment.id).filter(Comment.deleted_utc > 0).count(),
"comments last_24h": g.db.query(Comment.id).filter(Comment.created_utc > day, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count(),
"post votes": g.db.query(Vote.submission_id).count(),
"post voting users": g.db.query(Vote.user_id).distinct().count(),
"comment votes": g.db.query(CommentVote.comment_id).count(),
"comment voting users": g.db.query(CommentVote.user_id).distinct().count(),
"total upvotes": g.db.query(Vote.submission_id).filter_by(vote_type=1).count() + g.db.query(CommentVote.comment_id).filter_by(vote_type=1).count(),
"total downvotes": g.db.query(Vote.submission_id).filter_by(vote_type=-1).count() + g.db.query(CommentVote.comment_id).filter_by(vote_type=-1).count(),
"total awards": g.db.query(AwardRelationship.id).count(),
"awards given": g.db.query(AwardRelationship.id).filter(or_(AwardRelationship.submission_id != None, AwardRelationship.comment_id != None)).count(),
"users who posted, commented, or voted in the past 7 days": len(active_users),
}
g.db.commit()
return render_template("admin/content_stats.html", v=v, title="Content Statistics", data=stats)
@app.get("/chart")
def chart():
return redirect('/weekly_chart')
@app.get("/weekly_chart")
@auth_required
def weekly_chart(v):
file = cached_chart(kind="weekly", site=SITE)
f = send_file(file)
return f
@app.get("/daily_chart")
@auth_required
def daily_chart(v):
file = cached_chart(kind="daily", site=SITE)
f = send_file(file)
return f
@cache.memoize(timeout=86400)
def cached_chart(kind, site):
now = time.gmtime()
midnight_this_morning = time.struct_time((now.tm_year,
now.tm_mon,
now.tm_mday,
0,
0,
0,
now.tm_wday,
now.tm_yday,
0)
)
today_cutoff = calendar.timegm(midnight_this_morning)
if kind == "daily": day_cutoffs = [today_cutoff - 86400 * i for i in range(47)][1:]
else: day_cutoffs = [today_cutoff - 86400 * 7 * i for i in range(47)][1:]
day_cutoffs.insert(0, calendar.timegm(now))
daily_times = [time.strftime("%d/%m", time.gmtime(day_cutoffs[i + 1])) for i in range(len(day_cutoffs) - 1)][::-1]
daily_signups = [g.db.query(User.id).filter(User.created_utc < day_cutoffs[i], User.created_utc > day_cutoffs[i + 1]).count() for i in range(len(day_cutoffs) - 1)][::-1]
post_stats = [g.db.query(Submission.id).filter(Submission.created_utc < day_cutoffs[i], Submission.created_utc > day_cutoffs[i + 1], Submission.is_banned == False).count() for i in range(len(day_cutoffs) - 1)][::-1]
comment_stats = [g.db.query(Comment.id).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id.notin_((AUTOJANNY_ID,NOTIFICATIONS_ID))).count() for i in range(len(day_cutoffs) - 1)][::-1]
plt.rcParams["figure.figsize"] = (30, 20)
signup_chart = plt.subplot2grid((30, 20), (0, 0), rowspan=6, colspan=30)
posts_chart = plt.subplot2grid((30, 20), (10, 0), rowspan=6, colspan=30)
comments_chart = plt.subplot2grid((30, 20), (20, 0), rowspan=6, colspan=30)
signup_chart.grid(), posts_chart.grid(), comments_chart.grid()
signup_chart.plot(
daily_times,
daily_signups,
color='red')
posts_chart.plot(
daily_times,
post_stats,
color='blue')
comments_chart.plot(
daily_times,
comment_stats,
color='purple')
signup_chart.set_ylim(ymin=0)
posts_chart.set_ylim(ymin=0)
comments_chart.set_ylim(ymin=0)
signup_chart.set_ylabel("Signups")
posts_chart.set_ylabel("Posts")
comments_chart.set_ylabel("Comments")
comments_chart.set_xlabel("Time (UTC)")
signup_chart.legend(loc='upper left', frameon=True)
posts_chart.legend(loc='upper left', frameon=True)
comments_chart.legend(loc='upper left', frameon=True)
file = f"/{SITE}_{kind}.png"
plt.savefig(file)
plt.clf()
return file
@app.get("/patrons")
@app.get("/paypigs")
@admin_level_required(3)
def patrons(v):
users = g.db.query(User).filter(User.patron > 0).order_by(User.patron.desc(), User.id).all()
return render_template("patrons.html", v=v, users=users)
@app.get("/admins")
@app.get("/badmins")
@auth_required
def admins(v):
if v and v.admin_level > 2:
admins = g.db.query(User).filter(User.admin_level>1).order_by(User.truecoins.desc()).all()
admins += g.db.query(User).filter(User.admin_level==1).order_by(User.truecoins.desc()).all()
else: admins = g.db.query(User).filter(User.admin_level>0).order_by(User.truecoins.desc()).all()
return render_template("admins.html", v=v, admins=admins)
@app.get("/log")
@app.get("/modlog")
@auth_required
def log(v):
try: page = max(int(request.values.get("page", 1)), 1)
except: page = 1
admin = request.values.get("admin")
if admin: admin_id = get_id(admin)
else: admin_id = 0
kind = request.values.get("kind")
if v and v.admin_level > 1: types = ACTIONTYPES
else: types = ACTIONTYPES2
if kind not in types: kind = None
actions = g.db.query(ModAction)
if not (v and v.admin_level > 1):
actions = actions.filter(ModAction.kind.notin_(["shadowban","unshadowban","flair_post","edit_post"]))
if admin_id:
actions = actions.filter_by(user_id=admin_id)
kinds = set([x.kind for x in actions])
types2 = {}
for k,val in types.items():
if k in kinds: types2[k] = val
types = types2
if kind: actions = actions.filter_by(kind=kind)
actions = actions.order_by(ModAction.id.desc()).offset(25*(page-1)).limit(26).all()
next_exists=len(actions)>25
actions=actions[:25]
admins = [x[0] for x in g.db.query(User.username).filter(User.admin_level > 1).order_by(User.username).all()]
return render_template("log.html", v=v, admins=admins, types=types, admin=admin, type=kind, actions=actions, next_exists=next_exists, page=page)
@app.get("/log/<id>")
@auth_required
def log_item(id, v):
try: id = int(id)
except: abort(404)
action=g.db.query(ModAction).filter_by(id=id).one_or_none()
if not action: abort(404)
admins = [x[0] for x in g.db.query(User.username).filter(User.admin_level > 1).all()]
if v and v.admin_level > 1: types = ACTIONTYPES
else: types = ACTIONTYPES2
return render_template("log.html", v=v, actions=[action], next_exists=False, page=1, action=action, admins=admins, types=types)
@app.get("/api")
@auth_required
def api(v):
return render_template("api.html", v=v)
@app.get("/contact")
@app.get("/press")
@app.get("/media")
@auth_required
def contact(v):
return render_template("contact.html", v=v)
@app.post("/send_admin")
@limiter.limit("1/second;2/minute;6/hour;10/day")
@auth_required
def submit_contact(v):
body = request.values.get("message")
if not body: abort(400)
body = f'This message has been sent automatically to all admins via [/contact](/contact)\n\nMessage:\n\n' + body
body_html = sanitize(body)
if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1":
file=request.files["file"]
if file.content_type.startswith('image/'):
name = f'/images/{time.time()}'.replace('.','') + '.webp'
file.save(name)
url = process_image(name)
body_html += f'<img data-bs-target="#expandImageModal" data-bs-toggle="modal" onclick="expandDesktopImage(this.src)" class="img" src="{url}" loading="lazy">'
elif file.content_type.startswith('video/'):
file.save("video.mp4")
with open("video.mp4", 'rb') as f:
try: req = requests.request("POST", "https://api.imgur.com/3/upload", headers={'Authorization': f'Client-ID {IMGUR_KEY}'}, files=[('video', f)], timeout=5).json()['data']
except requests.Timeout: return {"error": "Video upload timed out, please try again!"}
try: url = req['link']
except:
error = req['error']
if error == 'File exceeds max duration': error += ' (60 seconds)'
return {"error": error}, 400
if url.endswith('.'): url += 'mp4'
body_html += f"<p>{url}</p>"
else: return {"error": "Image/Video files only"}, 400
new_comment = Comment(author_id=v.id,
parent_submission=None,
level=1,
body_html=body_html,
sentto=2
)
g.db.add(new_comment)
g.db.flush()
new_comment.top_comment_id = new_comment.id
for admin in g.db.query(User).filter(User.admin_level > 2).all():
notif = Notification(comment_id=new_comment.id, user_id=admin.id)
g.db.add(notif)
g.db.commit()
return render_template("contact.html", v=v, msg="Your message has been sent.")
@app.get('/archives')
def archivesindex():
return redirect("/archives/index.html")
@app.get('/archives/<path:path>')
def archives(path):
resp = make_response(send_from_directory('/archives', path))
if request.path.endswith('.css'): resp.headers.add("Content-Type", "text/css")
return resp
@app.get('/e/<emoji>')
@limiter.exempt
def emoji(emoji):
if not emoji.endswith('.webp'): abort(404)
resp = make_response(send_from_directory('assets/images/emojis', emoji))
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=3153600")
resp.headers.remove("Content-Type")
resp.headers.add("Content-Type", "image/webp")
return resp
@app.get('/assets/<path:path>')
@app.get('/static/assets/<path:path>')
@limiter.exempt
def static_service(path):
resp = make_response(send_from_directory('assets', path))
if request.path.endswith('.webp') or request.path.endswith('.gif') or request.path.endswith('.ttf') or request.path.endswith('.woff2'):
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=3153600")
if request.path.endswith('.webp'):
resp.headers.remove("Content-Type")
resp.headers.add("Content-Type", "image/webp")
return resp
@app.get('/images/<path>')
@app.get('/hostedimages/<path>')
@app.get("/static/images/<path>")
@limiter.exempt
def images(path):
resp = make_response(send_from_directory('/images', path.replace('.WEBP','.webp')))
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=3153600")
if request.path.endswith('.webp'):
resp.headers.remove("Content-Type")
resp.headers.add("Content-Type", "image/webp")
return resp
@app.get("/robots.txt")
def robots_txt():
try: f = send_file("assets/robots.txt")
except:
print('/robots.txt', flush=True)
abort(404)
return f
@app.get("/badges")
@auth_required
@cache.memoize(timeout=3600, make_name=make_name)
def badges(v):
badges = g.db.query(BadgeDef).order_by(BadgeDef.id).all()
counts_raw = g.db.query(Badge.badge_id, func.count()).group_by(Badge.badge_id).all()
users = g.db.query(User.id).count()
counts = {}
for c in counts_raw:
counts[c[0]] = (c[1], float(c[1]) * 100 / max(users, 1))
return render_template("badges.html", v=v, badges=badges, counts=counts)
@app.get("/blocks")
@auth_required
def blocks(v):
blocks=g.db.query(UserBlock).all()
users = []
targets = []
for x in blocks:
users.append(get_account(x.user_id))
targets.append(get_account(x.target_id))
return render_template("blocks.html", v=v, users=users, targets=targets)
@app.get("/banned")
@auth_required
def banned(v):
users = [x for x in g.db.query(User).filter(User.is_banned > 0, User.unban_utc == 0).all()]
return render_template("banned.html", v=v, users=users)
@app.get("/formatting")
@auth_required
def formatting(v):
return render_template("formatting.html", v=v)
@app.get("/service-worker.js")
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
)
@app.get("/.well-known/assetlinks.json")
def googleplayapp():
with open("files/assets/assetlinks.json", "r") as f:
return Response(f.read(), mimetype='application/json')
@app.post("/dismiss_mobile_tip")
def dismiss_mobile_tip():
session["tooltip_last_dismissed"] = int(time.time())
return "", 204

File diff suppressed because it is too large Load diff

View file

@ -1,278 +1,278 @@
from files.helpers.wrappers import *
from files.helpers.get import *
from files.helpers.const import *
from files.classes import *
from flask import *
from files.__main__ import app, limiter, cache
from os import environ
@app.get("/votes")
@limiter.limit("5/second;60/minute;200/hour;1000/day")
@auth_required
def admin_vote_info_get(v):
link = request.values.get("link")
if not link: return render_template("votes.html", v=v)
try:
if "t2_" in link: thing = get_post(int(link.split("t2_")[1]), v=v)
elif "t3_" in link: thing = get_comment(int(link.split("t3_")[1]), v=v)
else: abort(400)
except: abort(400)
if thing.ghost and v.id != AEVANN_ID: abort(403)
if not thing.author:
print(thing.id, flush=True)
if isinstance(thing, Submission):
if thing.author.shadowbanned and not (v and v.admin_level):
thing_id = g.db.query(Submission.id).filter_by(upvotes=thing.upvotes, downvotes=thing.downvotes).order_by(Submission.id).first()[0]
else: thing_id = thing.id
ups = g.db.query(Vote).filter_by(submission_id=thing_id, vote_type=1).order_by(Vote.created_utc).all()
downs = g.db.query(Vote).filter_by(submission_id=thing_id, vote_type=-1).order_by(Vote.created_utc).all()
elif isinstance(thing, Comment):
if thing.author.shadowbanned and not (v and v.admin_level):
thing_id = g.db.query(Comment.id).filter_by(upvotes=thing.upvotes, downvotes=thing.downvotes).order_by(Comment.id).first()[0]
else: thing_id = thing.id
ups = g.db.query(CommentVote).filter_by(comment_id=thing_id, vote_type=1).order_by(CommentVote.created_utc).all()
downs = g.db.query(CommentVote).filter_by(comment_id=thing_id, vote_type=-1 ).order_by(CommentVote.created_utc).all()
else: abort(400)
return render_template("votes.html",
v=v,
thing=thing,
ups=ups,
downs=downs)
@app.post("/vote/post/<post_id>/<new>")
@limiter.limit("5/second;60/minute;600/hour;1000/day")
@is_not_permabanned
def api_vote_post(post_id, new, v):
if new == "-1" and environ.get('DISABLE_DOWNVOTES') == '1': return {"error": "forbidden."}, 403
if new not in ["-1", "0", "1"]: abort(400)
if request.headers.get("Authorization"): abort(403)
new = int(new)
post = get_post(post_id)
existing = g.db.query(Vote).filter_by(user_id=v.id, submission_id=post.id).one_or_none()
coin_delta = 1
if v.id == post.author.id:
coin_delta = 0
if existing and existing.vote_type == new: return "", 204
if existing:
if existing.vote_type == 0 and new != 0:
post.author.coins += coin_delta
post.author.truecoins += coin_delta
g.db.add(post.author)
existing.vote_type = new
g.db.add(existing)
elif existing.vote_type != 0 and new == 0:
post.author.coins -= coin_delta
post.author.truecoins -= coin_delta
g.db.add(post.author)
g.db.delete(existing)
else:
existing.vote_type = new
g.db.add(existing)
elif new != 0:
post.author.coins += coin_delta
post.author.truecoins += coin_delta
g.db.add(post.author)
if new == 1 and (v.agendaposter or v.shadowbanned or (v.is_banned and not v.unban_utc) or (v.profile_url.startswith('/e/') and not v.customtitle and v.namecolor == DEFAULT_COLOR)): real = False
else: real = True
vote = Vote(user_id=v.id,
vote_type=new,
submission_id=post_id,
app_id=v.client.application.id if v.client else None,
real = real
)
g.db.add(vote)
g.db.flush()
post.upvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, vote_type=1).count()
post.downvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, vote_type=-1).count()
post.realupvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, real=True).count()
if post.author.progressivestack: post.realupvotes *= 2
g.db.add(post)
g.db.commit()
return "", 204
@app.post("/vote/comment/<comment_id>/<new>")
@limiter.limit("5/second;60/minute;600/hour;1000/day")
@is_not_permabanned
def api_vote_comment(comment_id, new, v):
if new == "-1" and environ.get('DISABLE_DOWNVOTES') == '1': return {"error": "forbidden."}, 403
if new not in ["-1", "0", "1"]: abort(400)
if request.headers.get("Authorization"): abort(403)
new = int(new)
try: comment_id = int(comment_id)
except: abort(404)
comment = get_comment(comment_id)
if comment.author_id in {AUTOPOLLER_ID,AUTOBETTER_ID,AUTOCHOICE_ID}: return {"error": "forbidden."}, 403
existing = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
coin_delta = 1
if v.id == comment.author_id:
coin_delta = 0
if existing and existing.vote_type == new: return "", 204
if existing:
if existing.vote_type == 0 and new != 0:
comment.author.coins += coin_delta
comment.author.truecoins += coin_delta
g.db.add(comment.author)
existing.vote_type = new
g.db.add(existing)
elif existing.vote_type != 0 and new == 0:
comment.author.coins -= coin_delta
comment.author.truecoins -= coin_delta
g.db.add(comment.author)
g.db.delete(existing)
else:
existing.vote_type = new
g.db.add(existing)
elif new != 0:
comment.author.coins += coin_delta
comment.author.truecoins += coin_delta
g.db.add(comment.author)
if new == 1 and (v.agendaposter or v.shadowbanned or (v.is_banned and not v.unban_utc) or (v.profile_url.startswith('/e/') and not v.customtitle and v.namecolor == DEFAULT_COLOR)): real = False
else: real = True
vote = CommentVote(user_id=v.id,
vote_type=new,
comment_id=comment_id,
app_id=v.client.application.id if v.client else None,
real=real
)
g.db.add(vote)
g.db.flush()
comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count()
comment.downvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=-1).count()
comment.realupvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, real=True).count()
if comment.author.progressivestack: comment.realupvotes *= 2
g.db.add(comment)
g.db.commit()
return "", 204
@app.post("/vote/poll/<comment_id>")
@is_not_permabanned
def api_vote_poll(comment_id, v):
vote = request.values.get("vote")
if vote == "true": new = 1
elif vote == "false": new = 0
else: abort(400)
comment_id = int(comment_id)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
if existing and existing.vote_type == new: return "", 204
if existing:
if new == 1:
existing.vote_type = new
g.db.add(existing)
else: g.db.delete(existing)
elif new == 1:
vote = CommentVote(user_id=v.id, vote_type=new, comment_id=comment.id)
g.db.add(vote)
g.db.flush()
comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count()
g.db.add(comment)
g.db.commit()
return "", 204
@app.post("/bet/<comment_id>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@is_not_permabanned
def bet(comment_id, v):
if v.coins < 200: return {"error": "You don't have 200 coins!"}
vote = request.values.get("vote")
comment_id = int(comment_id)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
if existing: return "", 204
vote = CommentVote(user_id=v.id, vote_type=1, comment_id=comment.id)
g.db.add(vote)
comment.upvotes += 1
g.db.add(comment)
v.coins -= 200
g.db.add(v)
autobetter = g.db.query(User).filter_by(id=AUTOBETTER_ID).one_or_none()
autobetter.coins += 200
g.db.add(autobetter)
g.db.commit()
return "", 204
@app.post("/vote/choice/<comment_id>")
@is_not_permabanned
def api_vote_choice(comment_id, v):
comment_id = int(comment_id)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
if existing and existing.vote_type == 1: return "", 204
if existing:
existing.vote_type = 1
g.db.add(existing)
else:
vote = CommentVote(user_id=v.id, vote_type=1, comment_id=comment.id)
g.db.add(vote)
if comment.parent_comment: parent = comment.parent_comment
else: parent = comment.post
for vote in parent.total_choice_voted(v):
vote.comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=vote.comment.id, vote_type=1).count() - 1
g.db.add(vote.comment)
g.db.delete(vote)
g.db.flush()
comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count()
g.db.add(comment)
g.db.commit()
from files.helpers.wrappers import *
from files.helpers.get import *
from files.helpers.const import *
from files.classes import *
from flask import *
from files.__main__ import app, limiter, cache
from os import environ
@app.get("/votes")
@limiter.limit("5/second;60/minute;200/hour;1000/day")
@auth_required
def admin_vote_info_get(v):
link = request.values.get("link")
if not link: return render_template("votes.html", v=v)
try:
if "t2_" in link: thing = get_post(int(link.split("t2_")[1]), v=v)
elif "t3_" in link: thing = get_comment(int(link.split("t3_")[1]), v=v)
else: abort(400)
except: abort(400)
if thing.ghost and v.id != AEVANN_ID: abort(403)
if not thing.author:
print(thing.id, flush=True)
if isinstance(thing, Submission):
if thing.author.shadowbanned and not (v and v.admin_level):
thing_id = g.db.query(Submission.id).filter_by(upvotes=thing.upvotes, downvotes=thing.downvotes).order_by(Submission.id).first()[0]
else: thing_id = thing.id
ups = g.db.query(Vote).filter_by(submission_id=thing_id, vote_type=1).order_by(Vote.created_utc).all()
downs = g.db.query(Vote).filter_by(submission_id=thing_id, vote_type=-1).order_by(Vote.created_utc).all()
elif isinstance(thing, Comment):
if thing.author.shadowbanned and not (v and v.admin_level):
thing_id = g.db.query(Comment.id).filter_by(upvotes=thing.upvotes, downvotes=thing.downvotes).order_by(Comment.id).first()[0]
else: thing_id = thing.id
ups = g.db.query(CommentVote).filter_by(comment_id=thing_id, vote_type=1).order_by(CommentVote.created_utc).all()
downs = g.db.query(CommentVote).filter_by(comment_id=thing_id, vote_type=-1 ).order_by(CommentVote.created_utc).all()
else: abort(400)
return render_template("votes.html",
v=v,
thing=thing,
ups=ups,
downs=downs)
@app.post("/vote/post/<post_id>/<new>")
@limiter.limit("5/second;60/minute;600/hour;1000/day")
@is_not_permabanned
def api_vote_post(post_id, new, v):
if new == "-1" and environ.get('DISABLE_DOWNVOTES') == '1': return {"error": "forbidden."}, 403
if new not in ["-1", "0", "1"]: abort(400)
if request.headers.get("Authorization"): abort(403)
new = int(new)
post = get_post(post_id)
existing = g.db.query(Vote).filter_by(user_id=v.id, submission_id=post.id).one_or_none()
coin_delta = 1
if v.id == post.author.id:
coin_delta = 0
if existing and existing.vote_type == new: return "", 204
if existing:
if existing.vote_type == 0 and new != 0:
post.author.coins += coin_delta
post.author.truecoins += coin_delta
g.db.add(post.author)
existing.vote_type = new
g.db.add(existing)
elif existing.vote_type != 0 and new == 0:
post.author.coins -= coin_delta
post.author.truecoins -= coin_delta
g.db.add(post.author)
g.db.delete(existing)
else:
existing.vote_type = new
g.db.add(existing)
elif new != 0:
post.author.coins += coin_delta
post.author.truecoins += coin_delta
g.db.add(post.author)
if new == 1 and (v.agendaposter or v.shadowbanned or (v.is_banned and not v.unban_utc) or (v.profile_url.startswith('/e/') and not v.customtitle and v.namecolor == DEFAULT_COLOR)): real = False
else: real = True
vote = Vote(user_id=v.id,
vote_type=new,
submission_id=post_id,
app_id=v.client.application.id if v.client else None,
real = real
)
g.db.add(vote)
g.db.flush()
post.upvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, vote_type=1).count()
post.downvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, vote_type=-1).count()
post.realupvotes = g.db.query(Vote.submission_id).filter_by(submission_id=post.id, real=True).count()
if post.author.progressivestack: post.realupvotes *= 2
g.db.add(post)
g.db.commit()
return "", 204
@app.post("/vote/comment/<comment_id>/<new>")
@limiter.limit("5/second;60/minute;600/hour;1000/day")
@is_not_permabanned
def api_vote_comment(comment_id, new, v):
if new == "-1" and environ.get('DISABLE_DOWNVOTES') == '1': return {"error": "forbidden."}, 403
if new not in ["-1", "0", "1"]: abort(400)
if request.headers.get("Authorization"): abort(403)
new = int(new)
try: comment_id = int(comment_id)
except: abort(404)
comment = get_comment(comment_id)
if comment.author_id in {AUTOPOLLER_ID,AUTOBETTER_ID,AUTOCHOICE_ID}: return {"error": "forbidden."}, 403
existing = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
coin_delta = 1
if v.id == comment.author_id:
coin_delta = 0
if existing and existing.vote_type == new: return "", 204
if existing:
if existing.vote_type == 0 and new != 0:
comment.author.coins += coin_delta
comment.author.truecoins += coin_delta
g.db.add(comment.author)
existing.vote_type = new
g.db.add(existing)
elif existing.vote_type != 0 and new == 0:
comment.author.coins -= coin_delta
comment.author.truecoins -= coin_delta
g.db.add(comment.author)
g.db.delete(existing)
else:
existing.vote_type = new
g.db.add(existing)
elif new != 0:
comment.author.coins += coin_delta
comment.author.truecoins += coin_delta
g.db.add(comment.author)
if new == 1 and (v.agendaposter or v.shadowbanned or (v.is_banned and not v.unban_utc) or (v.profile_url.startswith('/e/') and not v.customtitle and v.namecolor == DEFAULT_COLOR)): real = False
else: real = True
vote = CommentVote(user_id=v.id,
vote_type=new,
comment_id=comment_id,
app_id=v.client.application.id if v.client else None,
real=real
)
g.db.add(vote)
g.db.flush()
comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count()
comment.downvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=-1).count()
comment.realupvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, real=True).count()
if comment.author.progressivestack: comment.realupvotes *= 2
g.db.add(comment)
g.db.commit()
return "", 204
@app.post("/vote/poll/<comment_id>")
@is_not_permabanned
def api_vote_poll(comment_id, v):
vote = request.values.get("vote")
if vote == "true": new = 1
elif vote == "false": new = 0
else: abort(400)
comment_id = int(comment_id)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
if existing and existing.vote_type == new: return "", 204
if existing:
if new == 1:
existing.vote_type = new
g.db.add(existing)
else: g.db.delete(existing)
elif new == 1:
vote = CommentVote(user_id=v.id, vote_type=new, comment_id=comment.id)
g.db.add(vote)
g.db.flush()
comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count()
g.db.add(comment)
g.db.commit()
return "", 204
@app.post("/bet/<comment_id>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@is_not_permabanned
def bet(comment_id, v):
if v.coins < 200: return {"error": "You don't have 200 coins!"}
vote = request.values.get("vote")
comment_id = int(comment_id)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
if existing: return "", 204
vote = CommentVote(user_id=v.id, vote_type=1, comment_id=comment.id)
g.db.add(vote)
comment.upvotes += 1
g.db.add(comment)
v.coins -= 200
g.db.add(v)
autobetter = g.db.query(User).filter_by(id=AUTOBETTER_ID).one_or_none()
autobetter.coins += 200
g.db.add(autobetter)
g.db.commit()
return "", 204
@app.post("/vote/choice/<comment_id>")
@is_not_permabanned
def api_vote_choice(comment_id, v):
comment_id = int(comment_id)
comment = get_comment(comment_id)
existing = g.db.query(CommentVote).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()
if existing and existing.vote_type == 1: return "", 204
if existing:
existing.vote_type = 1
g.db.add(existing)
else:
vote = CommentVote(user_id=v.id, vote_type=1, comment_id=comment.id)
g.db.add(vote)
if comment.parent_comment: parent = comment.parent_comment
else: parent = comment.post
for vote in parent.total_choice_voted(v):
vote.comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=vote.comment.id, vote_type=1).count() - 1
g.db.add(vote.comment)
g.db.delete(vote)
g.db.flush()
comment.upvotes = g.db.query(CommentVote.comment_id).filter_by(comment_id=comment.id, vote_type=1).count()
g.db.add(comment)
g.db.commit()
return "", 204

View file

@ -1,84 +1,84 @@
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}}</title>
{% endblock %}
{% block content %}
<pre></pre>
<pre></pre>
<h3>&nbsp;Admin Tools</h3>
<h4>Content</h4>
<ul>
<li><a href="/admin/image_posts">Image Posts</a></li>
<li><a href="/admin/reported/posts">Reported Posts/Comments</a></li>
<li><a href="/admin/removed/posts">Removed Posts/Comments</a></li>
</ul>
<h4>Users</h4>
<ul>
<li><a href="/admin/users">Users Feed</a></li>
<li><a href="/admin/shadowbanned">Shadowbanned Users</a></li>
<li><a href="/banned">Permabanned Users</a></li>
<li><a href="/agendaposters">Users with Chud Theme</a></li>
<li><a href="/grassed">Currently Grassed Users</a></li>
</ul>
<h4>Safety</h4>
<ul>
<li><a href="/admin/banned_domains">Banned Domains</a></li>
<li><a href="/admin/alt_votes">Multi Vote Analysis</a></li>
</ul>
<h4>Grant</h4>
<ul>
<li><a href="/admin/awards">Give User Award</a></li>
<li><a href="/admin/badge_grant">Grant Badges</a></li>
<li><a href="/admin/badge_remove">Remove Badges</a></li>
</ul>
<h4>API Access Control</h4>
<ul>
<li><a href="/admin/apps">Apps</a></li>
</ul>
<h4>Statistics</h4>
<ul>
<li><a href="/stats">Content Stats</a></li>
<li><a href="/weekly_chart">Weekly Stat Chart</a></li>
<li><a href="/daily_chart">Daily Stat Chart</a></li>
</ul>
{% if v.admin_level > 2 %}
<pre></pre>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="signups" {% if site_settings['Signups'] %}checked{% endif %} onchange="post_toast(this,'/admin/site_settings/Signups');">
<label class="custom-control-label" for="signups">Signups</label>
</div>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="bots" {% if site_settings['Bots'] %}checked{% endif %} onchange="post_toast(this,'/admin/site_settings/Bots');">
<label class="custom-control-label" for="bots">Bots</label>
</div>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="Fart mode" {% if site_settings['Fart mode'] %}checked{% endif %} onchange="post_toast(this,'/admin/site_settings/Fart mode');">
<label class="custom-control-label" for="Fart mode">Fart mode</label>
</div>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="Read-only mode" {% if site_settings['Read-only mode'] %}checked{% endif %} onchange="post_toast(this,'/admin/site_settings/Read-only mode');">
<label class="custom-control-label" for="Read-only mode">Read-only mode</label>
</div>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="under_attack" name="under_attack" {% if under_attack%}checked{% endif %} onchange="post_toast(this,'/admin/under_attack');">
<label class="custom-control-label" for="under_attack">Under attack mode</label>
</div>
<button class="btn btn-primary mt-3" onclick="post_toast(this,'/admin/purge_cache');">PURGE CACHE</button>
{% endif %}
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}}</title>
{% endblock %}
{% block content %}
<pre></pre>
<pre></pre>
<h3>&nbsp;Admin Tools</h3>
<h4>Content</h4>
<ul>
<li><a href="/admin/image_posts">Image Posts</a></li>
<li><a href="/admin/reported/posts">Reported Posts/Comments</a></li>
<li><a href="/admin/removed/posts">Removed Posts/Comments</a></li>
</ul>
<h4>Users</h4>
<ul>
<li><a href="/admin/users">Users Feed</a></li>
<li><a href="/admin/shadowbanned">Shadowbanned Users</a></li>
<li><a href="/banned">Permabanned Users</a></li>
<li><a href="/agendaposters">Users with Chud Theme</a></li>
<li><a href="/grassed">Currently Grassed Users</a></li>
</ul>
<h4>Safety</h4>
<ul>
<li><a href="/admin/banned_domains">Banned Domains</a></li>
<li><a href="/admin/alt_votes">Multi Vote Analysis</a></li>
</ul>
<h4>Grant</h4>
<ul>
<li><a href="/admin/awards">Give User Award</a></li>
<li><a href="/admin/badge_grant">Grant Badges</a></li>
<li><a href="/admin/badge_remove">Remove Badges</a></li>
</ul>
<h4>API Access Control</h4>
<ul>
<li><a href="/admin/apps">Apps</a></li>
</ul>
<h4>Statistics</h4>
<ul>
<li><a href="/stats">Content Stats</a></li>
<li><a href="/weekly_chart">Weekly Stat Chart</a></li>
<li><a href="/daily_chart">Daily Stat Chart</a></li>
</ul>
{% if v.admin_level > 2 %}
<pre></pre>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="signups" {% if site_settings['Signups'] %}checked{% endif %} onchange="post_toast(this,'/admin/site_settings/Signups');">
<label class="custom-control-label" for="signups">Signups</label>
</div>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="bots" {% if site_settings['Bots'] %}checked{% endif %} onchange="post_toast(this,'/admin/site_settings/Bots');">
<label class="custom-control-label" for="bots">Bots</label>
</div>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="Fart mode" {% if site_settings['Fart mode'] %}checked{% endif %} onchange="post_toast(this,'/admin/site_settings/Fart mode');">
<label class="custom-control-label" for="Fart mode">Fart mode</label>
</div>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="Read-only mode" {% if site_settings['Read-only mode'] %}checked{% endif %} onchange="post_toast(this,'/admin/site_settings/Read-only mode');">
<label class="custom-control-label" for="Read-only mode">Read-only mode</label>
</div>
<div class="custom-control custom-switch">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="under_attack" name="under_attack" {% if under_attack%}checked{% endif %} onchange="post_toast(this,'/admin/under_attack');">
<label class="custom-control-label" for="under_attack">Under attack mode</label>
</div>
<button class="btn btn-primary mt-3" onclick="post_toast(this,'/admin/purge_cache');">PURGE CACHE</button>
{% endif %}
{% endblock %}

View file

@ -1,88 +1,88 @@
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}}</title>
{% endblock %}
{% block content %}
<pre>
</pre>
<h5>Vote Info</h5>
<form action="/admin/alt_votes" method="get" class="mb-6">
<label for="link-input">Usernames</label>
<input autocomplete="off" id="link-input" type="text" class="form-control mb-2" name="u1" value="{{u1.username if u1 else ''}}" placeholder="User 1">
<input autocomplete="off" id="link-input" type="text" class="form-control mb-2" name="u2" value="{{u2.username if u2 else ''}}" placeholder="User 2">
<input type="submit" value="Submit" class="btn btn-primary">
</form>
{% if u1 and u2 %}
<h2>Analysis</h2>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th></th>
<th>@{{u1.username}} only(% unique)</th>
<th>Both</th>
<th>@{{u2.username}} only (% unique)</th>
</tr>
</thead>
<tr>
<td><b>Post Upvotes</b></td>
<td>{{data['u1_only_post_ups']}} ({{data['u1_post_ups_unique']}}%)</td>
<td>{{data['both_post_ups']}}</td>
<td>{{data['u2_only_post_ups']}} ({{data['u2_post_ups_unique']}}%)</td>
</tr>
<tr>
<td><b>Post Downvotes</b></td>
<td>{{data['u1_only_post_downs']}} ({{data['u1_post_downs_unique']}}%)</td>
<td>{{data['both_post_downs']}}</td>
<td>{{data['u2_only_post_downs']}} ({{data['u2_post_downs_unique']}}%)</td>
</tr>
<tr>
<td><b>Comment Upvotes</b></td>
<td>{{data['u1_only_comment_ups']}} ({{data['u1_comment_ups_unique']}}%)</td>
<td>{{data['both_comment_ups']}}</td>
<td>{{data['u2_only_comment_ups']}} ({{data['u2_comment_ups_unique']}}%)</td>
</tr>
<tr>
<td><b>Comment Downvotes</b></td>
<td>{{data['u1_only_comment_downs']}} ({{data['u1_comment_downs_unique']}}%)</td>
<td>{{data['both_comment_downs']}}</td>
<td>{{data['u2_only_comment_downs']}} ({{data['u2_comment_downs_unique']}}%)</td>
</tr>
</table>
<h2>Link Accounts</h2>
{% if u2 in u1.alts %}
<p>Accounts are known alts of eachother.</p>
{% else %}
<p>Two accounts controlled by different people should have most uniqueness percentages at or above 70-80%</p>
<p>A sockpuppet account will have its uniqueness percentages significantly lower.</p>
<a role="button" class="btn btn-secondary" onclick="document.getElementById('linkbtn').classList.toggle('d-none');">Link Accounts</a>
<form action="/admin/link_accounts" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input type="hidden" name="u1" value="{{u1.id}}">
<input type="hidden" name="u2" value="{{u2.id}}">
<input type="submit" id="linkbtn" class="btn btn-primary d-none" value="Confirm Link: {{u1.username}} and {{u2.username}}">
</form>
{% endif %}
{% endif %}
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}}</title>
{% endblock %}
{% block content %}
<pre>
</pre>
<h5>Vote Info</h5>
<form action="/admin/alt_votes" method="get" class="mb-6">
<label for="link-input">Usernames</label>
<input autocomplete="off" id="link-input" type="text" class="form-control mb-2" name="u1" value="{{u1.username if u1 else ''}}" placeholder="User 1">
<input autocomplete="off" id="link-input" type="text" class="form-control mb-2" name="u2" value="{{u2.username if u2 else ''}}" placeholder="User 2">
<input type="submit" value="Submit" class="btn btn-primary">
</form>
{% if u1 and u2 %}
<h2>Analysis</h2>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th></th>
<th>@{{u1.username}} only(% unique)</th>
<th>Both</th>
<th>@{{u2.username}} only (% unique)</th>
</tr>
</thead>
<tr>
<td><b>Post Upvotes</b></td>
<td>{{data['u1_only_post_ups']}} ({{data['u1_post_ups_unique']}}%)</td>
<td>{{data['both_post_ups']}}</td>
<td>{{data['u2_only_post_ups']}} ({{data['u2_post_ups_unique']}}%)</td>
</tr>
<tr>
<td><b>Post Downvotes</b></td>
<td>{{data['u1_only_post_downs']}} ({{data['u1_post_downs_unique']}}%)</td>
<td>{{data['both_post_downs']}}</td>
<td>{{data['u2_only_post_downs']}} ({{data['u2_post_downs_unique']}}%)</td>
</tr>
<tr>
<td><b>Comment Upvotes</b></td>
<td>{{data['u1_only_comment_ups']}} ({{data['u1_comment_ups_unique']}}%)</td>
<td>{{data['both_comment_ups']}}</td>
<td>{{data['u2_only_comment_ups']}} ({{data['u2_comment_ups_unique']}}%)</td>
</tr>
<tr>
<td><b>Comment Downvotes</b></td>
<td>{{data['u1_only_comment_downs']}} ({{data['u1_comment_downs_unique']}}%)</td>
<td>{{data['both_comment_downs']}}</td>
<td>{{data['u2_only_comment_downs']}} ({{data['u2_comment_downs_unique']}}%)</td>
</tr>
</table>
<h2>Link Accounts</h2>
{% if u2 in u1.alts %}
<p>Accounts are known alts of eachother.</p>
{% else %}
<p>Two accounts controlled by different people should have most uniqueness percentages at or above 70-80%</p>
<p>A sockpuppet account will have its uniqueness percentages significantly lower.</p>
<a role="button" class="btn btn-secondary" onclick="document.getElementById('linkbtn').classList.toggle('d-none');">Link Accounts</a>
<form action="/admin/link_accounts" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input type="hidden" name="u1" value="{{u1.id}}">
<input type="hidden" name="u2" value="{{u2.id}}">
<input type="submit" id="linkbtn" class="btn btn-primary d-none" value="Confirm Link: {{u1.username}} and {{u2.username}}">
</form>
{% endif %}
{% endif %}
{% endblock %}

View file

@ -1,72 +1,72 @@
{% extends "default.html" %}
{% block title %}
<title>API App Administration</title>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="over18">{{app.app_name}}</label>
</div>
<div class="body w-lg-100">
<label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label>
<input autocomplete="off" id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly>
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label>
<input autocomplete="off" id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly>
<label for="edit-{{app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
<input autocomplete="off" id="edit-{{app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{app.redirect_uri}}" readonly="readonly">
<label for="edit-{{app.id}}-desc" class="mb-0 w-lg-25">Description</label>
<textarea rows="10" autocomplete="off" form="edit-app-{{app.id}}" class="form-control" name="description" id="edit-{{app.id}}-desc" maxlength="256" readonly="readonly">{{app.description}}</textarea>
</div>
</div>
<div class="footer">
<div class="d-flex">
{% if not app.client_id%}
<a role="button" class="btn btn-primary ml-auto" onclick="post_toast(this,'/admin/app/approve/{{app.id}}')">Approve</a>
<a role="button" class="btn btn-secondary mr-0" onclick="post_toast(this,'/admin/app/reject/{{app.id}}')">Reject</a>
{% else %}
<a role="button" class="btn btn-primary ml-auto" onclick="post_toast(this,'/admin/app/revoke/{{app.id}}')">Revoke</a>
{% endif %}
</div>
</div>
</div>
</div>
{% if listing %}
{% include "submission_listing.html" %}
{% elif comments %}
{% include "comments.html" %}
{% endif %}
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text">Action successful!</span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
{% extends "default.html" %}
{% block title %}
<title>API App Administration</title>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="over18">{{app.app_name}}</label>
</div>
<div class="body w-lg-100">
<label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label>
<input autocomplete="off" id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly>
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label>
<input autocomplete="off" id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly>
<label for="edit-{{app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
<input autocomplete="off" id="edit-{{app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{app.redirect_uri}}" readonly="readonly">
<label for="edit-{{app.id}}-desc" class="mb-0 w-lg-25">Description</label>
<textarea rows="10" autocomplete="off" form="edit-app-{{app.id}}" class="form-control" name="description" id="edit-{{app.id}}-desc" maxlength="256" readonly="readonly">{{app.description}}</textarea>
</div>
</div>
<div class="footer">
<div class="d-flex">
{% if not app.client_id%}
<a role="button" class="btn btn-primary ml-auto" onclick="post_toast(this,'/admin/app/approve/{{app.id}}')">Approve</a>
<a role="button" class="btn btn-secondary mr-0" onclick="post_toast(this,'/admin/app/reject/{{app.id}}')">Reject</a>
{% else %}
<a role="button" class="btn btn-primary ml-auto" onclick="post_toast(this,'/admin/app/revoke/{{app.id}}')">Revoke</a>
{% endif %}
</div>
</div>
</div>
</div>
{% if listing %}
{% include "submission_listing.html" %}
{% elif comments %}
{% include "comments.html" %}
{% endif %}
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text">Action successful!</span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
{% endblock %}

View file

@ -1,72 +1,72 @@
{% extends "default.html" %}
{% block title %}
<title>API App Administration</title>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
{% for app in apps %}
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="over18"><a href="{{app.permalink}}" {% if v and v.newtab and not g.webview %}target="_blank"{% endif %}>{{app.app_name}}</a></label>
</div>
<div class="body w-lg-100">
<label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label>
<input autocomplete="off" id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly>
<label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label>
<input autocomplete="off" id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly>
{% if app.client_id %}
<label for="edit-{{app.id}}-client-id" class="mb-0 w-lg-25">Client ID</label>
<input autocomplete="off" id="edit-{{app.id}}-client-id" class="form-control" type="text" name="name" value="{{app.client_id}}" readonly="readonly">
{% endif %}
<label for="edit-{{app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
<input autocomplete="off" id="edit-{{app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{app.redirect_uri}}" readonly="readonly">
<label for="edit-{{app.id}}-desc" class="mb-0 w-lg-25">Description</label>
<textarea rows="10" autocomplete="off" form="edit-app-{{app.id}}" class="form-control" name="description" id="edit-{{app.id}}-desc" maxlength="256" readonly="readonly">{{app.description}}</textarea>
</div>
</div>
<div class="footer">
<div class="d-flex">
{% if not app.client_id %}
<a role="button" class="btn btn-primary ml-auto" onclick="post_toast(this,'/admin/app/approve/{{app.id}}')">Approve</a>
<a role="button" class="btn btn-secondary mr-0" onclick="post_toast(this,'/admin/app/reject/{{app.id}}')">Reject</a>
{% else %}
<a role="button" class="btn btn-primary ml-auto" onclick="post_toast(this,'/admin/app/revoke/{{app.id}}')">Revoke</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text">Action successful!</span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
{% extends "default.html" %}
{% block title %}
<title>API App Administration</title>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-lg-8">
<div class="settings">
{% for app in apps %}
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="title w-lg-25">
<label for="over18"><a href="{{app.permalink}}" {% if v and v.newtab and not g.webview %}target="_blank"{% endif %}>{{app.app_name}}</a></label>
</div>
<div class="body w-lg-100">
<label for="edit-{{app.id}}-author" class="mb-0 w-lg-25">User</label>
<input autocomplete="off" id="edit-{{app.id}}-author" class="form-control" type="text" name="name" value="{{app.author.username}}" readonly=readonly>
<label for="edit-{{app.id}}-name" class="mb-0 w-lg-25">App Name</label>
<input autocomplete="off" id="edit-{{app.id}}-name" class="form-control" type="text" name="name" value="{{app.app_name}}" readonly=readonly>
{% if app.client_id %}
<label for="edit-{{app.id}}-client-id" class="mb-0 w-lg-25">Client ID</label>
<input autocomplete="off" id="edit-{{app.id}}-client-id" class="form-control" type="text" name="name" value="{{app.client_id}}" readonly="readonly">
{% endif %}
<label for="edit-{{app.id}}-redirect" class="mb-0 w-lg-25">Redirect URI</label>
<input autocomplete="off" id="edit-{{app.id}}-redirect" class="form-control" type="text" name="redirect_uri" value="{{app.redirect_uri}}" readonly="readonly">
<label for="edit-{{app.id}}-desc" class="mb-0 w-lg-25">Description</label>
<textarea rows="10" autocomplete="off" form="edit-app-{{app.id}}" class="form-control" name="description" id="edit-{{app.id}}-desc" maxlength="256" readonly="readonly">{{app.description}}</textarea>
</div>
</div>
<div class="footer">
<div class="d-flex">
{% if not app.client_id %}
<a role="button" class="btn btn-primary ml-auto" onclick="post_toast(this,'/admin/app/approve/{{app.id}}')">Approve</a>
<a role="button" class="btn btn-secondary mr-0" onclick="post_toast(this,'/admin/app/reject/{{app.id}}')">Reject</a>
{% else %}
<a role="button" class="btn btn-primary ml-auto" onclick="post_toast(this,'/admin/app/revoke/{{app.id}}')">Revoke</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text">Action successful!</span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
{% endblock %}

View file

@ -1,89 +1,89 @@
{% extends "default.html" %}
{% block title %}
<title>Badge Grant</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
{% if error %}
<div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<pre></pre>
<pre></pre>
<h5>Badge Grant</h5>
<form action="/admin/badge_grant", method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="input-username">Username</label><br>
<input autocomplete="off" id="input-username" class="form-control" type="text" name="username" required>
<div class="overflow-x-auto"><table class="table table-striped">
<thead class="bg-primary text-white">
<tr>
<th scope="col">Select</th>
<th scope="col">Image</th>
<th scope="col">Name</th>
<th scope="col">Default Description</th>
</tr>
</thead>
<tbody>
{% for badge in badge_types %}
<tr>
<td>
<div class="custom-control">
<input autocomplete="off" class="custom-control-input" type="radio" id="{{badge.id}}" name="badge_id" value="{{badge.id}}">
<label class="custom-control-label" for="{{badge.id}}"></label>
</div>
</td>
<td><label for="badge-{{badge.id}}"><img alt="{{badge.name}}" loading="lazy" src="/assets/images/badges/{{badge.id}}.webp?v=1016" width=64.16 height=70></label></td>
<td>{{badge.name}}</td>
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
<label for="input-url">URL</label><br>
<input autocomplete="off" id="input-url" class="form-control" type="text" name="url" type="url" placeholder="Optional">
<label for="input-description">Custom description</label><br>
<input autocomplete="off" id="input-description" class="form-control" type="text" name="description" placeholder="Leave blank for badge default">
<input autocomplete="off" class="btn btn-primary" type="submit">
</form>
<style>
@media (max-width: 767.98px) {
table {
display: inline-block;
overflow: auto;
}
}
</style>
{% endblock %}
{% extends "default.html" %}
{% block title %}
<title>Badge Grant</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
{% if error %}
<div class="alert alert-danger alert-dismissible fade show my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<pre></pre>
<pre></pre>
<h5>Badge Grant</h5>
<form action="/admin/badge_grant", method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="input-username">Username</label><br>
<input autocomplete="off" id="input-username" class="form-control" type="text" name="username" required>
<div class="overflow-x-auto"><table class="table table-striped">
<thead class="bg-primary text-white">
<tr>
<th scope="col">Select</th>
<th scope="col">Image</th>
<th scope="col">Name</th>
<th scope="col">Default Description</th>
</tr>
</thead>
<tbody>
{% for badge in badge_types %}
<tr>
<td>
<div class="custom-control">
<input autocomplete="off" class="custom-control-input" type="radio" id="{{badge.id}}" name="badge_id" value="{{badge.id}}">
<label class="custom-control-label" for="{{badge.id}}"></label>
</div>
</td>
<td><label for="badge-{{badge.id}}"><img alt="{{badge.name}}" loading="lazy" src="/assets/images/badges/{{badge.id}}.webp?v=1016" width=64.16 height=70></label></td>
<td>{{badge.name}}</td>
<td>{{badge.description}}</td>
</tr>
{% endfor %}
</table>
<label for="input-url">URL</label><br>
<input autocomplete="off" id="input-url" class="form-control" type="text" name="url" type="url" placeholder="Optional">
<label for="input-description">Custom description</label><br>
<input autocomplete="off" id="input-description" class="form-control" type="text" name="description" placeholder="Leave blank for badge default">
<input autocomplete="off" class="btn btn-primary" type="submit">
</form>
<style>
@media (max-width: 767.98px) {
table {
display: inline-block;
overflow: auto;
}
}
</style>
{% endblock %}

View file

@ -1,37 +1,37 @@
{% extends "default.html" %}
{% block title %}
<title>Banned Domains</title>
{% endblock %}
{% block content %}
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Domain</th>
<th>Ban reason</th>
</tr>
</thead>
{% for domain in banned_domains %}
<tr>
<td>{{domain.domain}}</td>
<td>{{domain.reason}}</td>
</tr>
{% endfor %}
</table>
<form action="/admin/banned_domains" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input autocomplete="off" name="domain" placeholder="Enter domain here.." class="form-control" required>
<input autocomplete="off" name="reason" placeholder="Enter ban reason here.." oninput="document.getElementById('ban-submit').disabled=false" class="form-control">
<input autocomplete="off" id="ban-submit" type="submit" class="btn btn-primary" value="Toggle ban" disabled>
</form>
{% extends "default.html" %}
{% block title %}
<title>Banned Domains</title>
{% endblock %}
{% block content %}
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Domain</th>
<th>Ban reason</th>
</tr>
</thead>
{% for domain in banned_domains %}
<tr>
<td>{{domain.domain}}</td>
<td>{{domain.reason}}</td>
</tr>
{% endfor %}
</table>
<form action="/admin/banned_domains" method="post">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<input autocomplete="off" name="domain" placeholder="Enter domain here.." class="form-control" required>
<input autocomplete="off" name="reason" placeholder="Enter ban reason here.." oninput="document.getElementById('ban-submit').disabled=false" class="form-control">
<input autocomplete="off" id="ban-submit" type="submit" class="btn btn-primary" value="Toggle ban" disabled>
</form>
{% endblock %}

View file

@ -1,25 +1,25 @@
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}}</title>
{% endblock %}
{% block content %}
<pre></pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Statistic</th>
<th>Value</th>
</tr>
</thead>
{% for entry in data %}
<tr>
<td>{{entry}}</td>
<td>{{data[entry]}}</td>
</tr>
{% endfor %}
</table>
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}}</title>
{% endblock %}
{% block content %}
<pre></pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>Statistic</th>
<th>Value</th>
</tr>
</thead>
{% for entry in data %}
<tr>
<td>{{entry}}</td>
<td>{{data[entry]}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,56 +1,56 @@
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block postNav %}{% endblock %}
{% block fixedMobileBarJS %}
{% endblock %}
{% block title %}
<title>Image feed</title>
{% endblock %}
{% block content %}
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block postNav %}{% endblock %}
{% block fixedMobileBarJS %}
{% endblock %}
{% block title %}
<title>Image feed</title>
{% endblock %}
{% block content %}
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends "mine.html" %}
{% block maincontent %}
{% include "user_listing.html" %}
{% endblock %}
{% extends "mine.html" %}
{% block maincontent %}
{% include "user_listing.html" %}
{% endblock %}
{% block navbar %}{% endblock %}

View file

@ -1,66 +1,66 @@
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block fixedMobileBarJS %}
{% endblock %}
{% block title %}
<title>removed Posts</title>
{% endblock %}
{% block content %}
<ul class="nav post-nav py-2">
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/removed/posts" %} active{% endif %}" href="/admin/removed/posts">
<div>Posts</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/removed/comments" %} active{% endif %}" href="/admin/removed/comments">
<div>Comments</div>
</a>
</li>
</ul>
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block fixedMobileBarJS %}
{% endblock %}
{% block title %}
<title>removed Posts</title>
{% endblock %}
{% block content %}
<ul class="nav post-nav py-2">
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/removed/posts" %} active{% endif %}" href="/admin/removed/posts">
<div>Posts</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/removed/comments" %} active{% endif %}" href="/admin/removed/comments">
<div>Comments</div>
</a>
</li>
</ul>
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}

View file

@ -1,26 +1,26 @@
{% extends "admin/reported_posts.html" %}
{% block title %}
<title>Comments</title>
{% endblock %}
{% block listing %}
<div class="posts">
{% with comments=listing %}
{% include "comments.html" %}
{% endwith %}
{% if not listing %}
<div class="row no-gutters">
<div class="col">
<div class="text-center py-7">
<div class="h4 p-2">There are no comments here (yet).</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% extends "admin/reported_posts.html" %}
{% block title %}
<title>Comments</title>
{% endblock %}
{% block listing %}
<div class="posts">
{% with comments=listing %}
{% include "comments.html" %}
{% endwith %}
{% if not listing %}
<div class="row no-gutters">
<div class="col">
<div class="text-center py-7">
<div class="h4 p-2">There are no comments here (yet).</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -1,66 +1,66 @@
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block fixedMobileBarJS %}
{% endblock %}
{% block title %}
<title>Reported Posts</title>
{% endblock %}
{% block content %}
<ul class="nav post-nav py-2">
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/reported/posts" %} active{% endif %}" href="/admin/reported/posts">
<div>Posts</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/reported/comments" %} active{% endif %}" href="/admin/reported/comments">
<div>Comments</div>
</a>
</li>
</ul>
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% extends "userpage.html" %}
{% block adminpanel %}{% endblock %}
{% block pagetype %}userpage{% endblock %}
{% block banner %}{% endblock %}
{% block mobileBanner %}{% endblock %}
{% block desktopBanner %}{% endblock %}
{% block desktopUserBanner %}{% endblock %}
{% block mobileUserBanner %}{% endblock %}
{% block fixedMobileBarJS %}
{% endblock %}
{% block title %}
<title>Reported Posts</title>
{% endblock %}
{% block content %}
<ul class="nav post-nav py-2">
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/reported/posts" %} active{% endif %}" href="/admin/reported/posts">
<div>Posts</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path=="/admin/reported/comments" %} active{% endif %}" href="/admin/reported/comments">
<div>Comments</div>
</a>
</li>
</ul>
<div class="row no-gutters">
<div class="col">
{% block listing %}
<div class="posts">
{% include "submission_listing.html" %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block pagenav %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm py-3 pl-3 mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?page={{page-1}}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?page={{page+1}}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endblock %}

View file

@ -1,31 +1,31 @@
{% extends "settings2.html" %}
{% block pagetitle %}Admins{% endblock %}
{% block content %}
<script src="/assets/js/sort_table.js?v=242"></script>
<pre class="d-none d-md-inline-block"></pre>
<h5 style="font-weight:bold;">Admins</h5>
<pre></pre>
<div class="overflow-x-auto">
<table id="sortable_table" class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th role="button" onclick="sort_table(2)" style="text-align:right;">Truescore</th>
<th role="button" onclick="sort_table(3)" style="text-align:right;">Mod actions</th>
</tr>
</thead>
{% for user in admins %}
<tr>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}}" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a>{% if user.admin_level == 1 and v and v.admin_level > 1 %}<i class="fas fa-broom align-middle ml-2 color-white" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Meme Admin"></i>{% endif %}</td>
<td style="text-align:right;">{{user.truecoins}}</td>
<td style="text-align:right;"><a href="/log?admin={{user.username}}">{{user.modaction_num}}</a></td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% extends "settings2.html" %}
{% block pagetitle %}Admins{% endblock %}
{% block content %}
<script src="/assets/js/sort_table.js?v=242"></script>
<pre class="d-none d-md-inline-block"></pre>
<h5 style="font-weight:bold;">Admins</h5>
<pre></pre>
<div class="overflow-x-auto">
<table id="sortable_table" class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th role="button" onclick="sort_table(2)" style="text-align:right;">Truescore</th>
<th role="button" onclick="sort_table(3)" style="text-align:right;">Mod actions</th>
</tr>
</thead>
{% for user in admins %}
<tr>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}}" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a>{% if user.admin_level == 1 and v and v.admin_level > 1 %}<i class="fas fa-broom align-middle ml-2 color-white" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Meme Admin"></i>{% endif %}</td>
<td style="text-align:right;">{{user.truecoins}}</td>
<td style="text-align:right;"><a href="/log?admin={{user.username}}">{{user.modaction_num}}</a></td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,112 +1,112 @@
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}} - API</title>
{% endblock %}
{% block content %}
<pre>
</pre>
<h1>API Guide for Bots</h1>
<pre></pre>
<p>This page explains how to obtain and use an access token. </p>
<h2>Step 1: Create your Application</h2>
<p>In the <a href="/settings/apps">apps tab of {{SITE_NAME}} settings</a>, fill in and submit the form to request an access token. You will need:</p>
<ul>
<li>an application name</li>
<li>a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).</li>
<li>a brief description of what your bot is intended to do</li>
</ul>
<p>Don't worry too much about accuracy; you will be able to change all of these later.</p>
<p>{{SITE_NAME}} administrators will review and approve or deny your request for an access token. You'll know when your request has been approved when you get a private message with an access token tied to your account.</p>
<p>DO NOT reveal your Client ID or Access Token. Anyone with these information will be able to pretend to be you. You are responsible for keeping them a secret!</p>
<h2>Step 2: Using the Access Token</h2>
<p>To use the access token, include the following header in subsequent API requests to {{SITE_NAME}}: <code>Authorization: access_token_goes_here</code></p>
<p>Python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="{{SITE_FULL}}/?sort=comments"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a large JSON representation of the posts on the frontpage sorted by the number of comments</p>
<br>
<p>Aother python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="{{SITE_FULL}}/unread"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a JSON representation of unread notifications for your account</p>
<pre>
</pre>
<h1>API Guide for Applications</h1>
<pre></pre>
<p>The OAuth2 authorization flow is used to enable users to authorize third-party applications to access their {{SITE_NAME}} account without having to provide their login information to the application.</p>
<p>This page explains how to obtain API application keys, how to prompt a user for authorization, and how to obtain and use access tokens. </p>
<h2>Step 1: Create your Application</h2>
<p>In the <a href="/settings/apps">apps tab of {{SITE_NAME}} settings</a>, fill in and submit the form to request new API keys. You will need:</p>
<ul>
<li>an application name</li>
<li>a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).</li>
<li>a brief description of what your application is intended to do</li>
</ul>
<p>Don't worry too much about accuracy; you will be able to change all of these later.</p>
<p>{{SITE_NAME}} administrators will review and approve or deny your request for API keys. You'll know when your request has been approved when you get a private message with an access token tied to your account.</p>
<p>DO NOT reveal your Client ID or Access Token. Anyone with these information will be able to pretend to be you. You are responsible for keeping them a secret!</p>
<h2>Step 2: Prompt Your User for Authorization</h2>
<p>Send your user to <code>{{SITE_FULL}}/authorize/?client_id=YOUR_CLIENT_ID</code></p>
<p>If done correctly, the user will see that your application wants to access their {{SITE_NAME}} account, and be prompted to approve or deny the request.</p>
<h2>Step 3: Catch the redirect</h2>
<p>The user clicks "Authorize". {{SITE_NAME}} will redirect the user's browser to GET the designated redirect URI. The access token URL parameter will be included in the redirect, which your server should process.</p>
<h2>Step 4: Using the Access Token</h2>
<p>To use the access token, include the following header in subsequent API requests to {{SITE_NAME}}: <code>Authorization: access_token_goes_here</code></p>
<p>Python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="{{SITE_FULL}}/?sort=comments"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a large JSON representation of the posts on the frontpage sorted by the number of comments</p>
<br>
<p>Aother python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="{{SITE_FULL}}/unread"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a JSON representation of unread notifications for your account</p>
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}} - API</title>
{% endblock %}
{% block content %}
<pre>
</pre>
<h1>API Guide for Bots</h1>
<pre></pre>
<p>This page explains how to obtain and use an access token. </p>
<h2>Step 1: Create your Application</h2>
<p>In the <a href="/settings/apps">apps tab of {{SITE_NAME}} settings</a>, fill in and submit the form to request an access token. You will need:</p>
<ul>
<li>an application name</li>
<li>a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).</li>
<li>a brief description of what your bot is intended to do</li>
</ul>
<p>Don't worry too much about accuracy; you will be able to change all of these later.</p>
<p>{{SITE_NAME}} administrators will review and approve or deny your request for an access token. You'll know when your request has been approved when you get a private message with an access token tied to your account.</p>
<p>DO NOT reveal your Client ID or Access Token. Anyone with these information will be able to pretend to be you. You are responsible for keeping them a secret!</p>
<h2>Step 2: Using the Access Token</h2>
<p>To use the access token, include the following header in subsequent API requests to {{SITE_NAME}}: <code>Authorization: access_token_goes_here</code></p>
<p>Python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="{{SITE_FULL}}/?sort=comments"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a large JSON representation of the posts on the frontpage sorted by the number of comments</p>
<br>
<p>Aother python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="{{SITE_FULL}}/unread"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a JSON representation of unread notifications for your account</p>
<pre>
</pre>
<h1>API Guide for Applications</h1>
<pre></pre>
<p>The OAuth2 authorization flow is used to enable users to authorize third-party applications to access their {{SITE_NAME}} account without having to provide their login information to the application.</p>
<p>This page explains how to obtain API application keys, how to prompt a user for authorization, and how to obtain and use access tokens. </p>
<h2>Step 1: Create your Application</h2>
<p>In the <a href="/settings/apps">apps tab of {{SITE_NAME}} settings</a>, fill in and submit the form to request new API keys. You will need:</p>
<ul>
<li>an application name</li>
<li>a Redirect URI. May not use HTTP unless using localhost (use HTTPS instead).</li>
<li>a brief description of what your application is intended to do</li>
</ul>
<p>Don't worry too much about accuracy; you will be able to change all of these later.</p>
<p>{{SITE_NAME}} administrators will review and approve or deny your request for API keys. You'll know when your request has been approved when you get a private message with an access token tied to your account.</p>
<p>DO NOT reveal your Client ID or Access Token. Anyone with these information will be able to pretend to be you. You are responsible for keeping them a secret!</p>
<h2>Step 2: Prompt Your User for Authorization</h2>
<p>Send your user to <code>{{SITE_FULL}}/authorize/?client_id=YOUR_CLIENT_ID</code></p>
<p>If done correctly, the user will see that your application wants to access their {{SITE_NAME}} account, and be prompted to approve or deny the request.</p>
<h2>Step 3: Catch the redirect</h2>
<p>The user clicks "Authorize". {{SITE_NAME}} will redirect the user's browser to GET the designated redirect URI. The access token URL parameter will be included in the redirect, which your server should process.</p>
<h2>Step 4: Using the Access Token</h2>
<p>To use the access token, include the following header in subsequent API requests to {{SITE_NAME}}: <code>Authorization: access_token_goes_here</code></p>
<p>Python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="{{SITE_FULL}}/?sort=comments"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a large JSON representation of the posts on the frontpage sorted by the number of comments</p>
<br>
<p>Aother python example:</p>
<pre> import requests
headers={"Authorization": "access_token_goes_here"}
url="{{SITE_FULL}}/unread"
r=requests.get(url, headers=headers)
print(r.json())
</pre>
<p>The expected result of this would be a JSON representation of unread notifications for your account</p>
{% endblock %}

View file

@ -1,125 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none';">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<title>{% block pagetitle %}{{SITE_NAME}}{% endblock %}</title>
{% if v %}
<style>:root{--primary:#{{v.themecolor}}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=250">
<link rel="stylesheet" href="/assets/css/{{v.theme}}.css?v=56">
{% if v.agendaposter %}
<style>
html {
cursor:url('/assets/images/dildo.webp?v=1008'), auto;
}
.nav-item .text-small.font-weight-bold::before {
content: "((("
}
.nav-item .text-small.font-weight-bold::after {
content: ")))"
}
.nav-item .text-small-extra.text-primary {
font-size: 0 !important
}
.nav-item .text-small-extra.text-primary i {
font-size: 11px !important
}
</style>
{% elif v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=250">
<link rel="stylesheet" href="/assets/css/{{config('DEFAULT_THEME')}}.css?v=56">
{% endif %}
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid">
<button class="navbar-toggler d-none" role="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="mb-5">
<a href="/" class="text-decoration-none"><span class="h3 text-primary"></span></a>
</div>
<h1 class="h2">{% block authtitle %}{% endblock %}</h1>
<p class="text-muted mb-md-5">{% block authtext %}{% endblock %}</p>
{% if error %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-info-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img alt="cover" loading="lazy" class="splash-img" src="/assets/images/{{SITE_NAME}}/cover.webp?v=1014"></img>
</div>
</div>
</div>
</div>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none';">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<title>{% block pagetitle %}{{SITE_NAME}}{% endblock %}</title>
{% if v %}
<style>:root{--primary:#{{v.themecolor}}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=250">
<link rel="stylesheet" href="/assets/css/{{v.theme}}.css?v=56">
{% if v.agendaposter %}
<style>
html {
cursor:url('/assets/images/dildo.webp?v=1008'), auto;
}
.nav-item .text-small.font-weight-bold::before {
content: "((("
}
.nav-item .text-small.font-weight-bold::after {
content: ")))"
}
.nav-item .text-small-extra.text-primary {
font-size: 0 !important
}
.nav-item .text-small-extra.text-primary i {
font-size: 11px !important
}
</style>
{% elif v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=250">
<link rel="stylesheet" href="/assets/css/{{config('DEFAULT_THEME')}}.css?v=56">
{% endif %}
</head>
<body id="login">
<nav class="navbar navbar-expand-lg navbar-dark bg-transparent fixed-top border-0">
<div class="container-fluid">
<button class="navbar-toggler d-none" role="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container-fluid position-absolute h-100 p-0">
<div class="row no-gutters h-100">
<div class="col-12 col-md-6 my-auto p-3">
<div class="row justify-content-center">
<div class="col-10 col-md-7">
<div class="mb-5">
<a href="/" class="text-decoration-none"><span class="h3 text-primary"></span></a>
</div>
<h1 class="h2">{% block authtitle %}{% endblock %}</h1>
<p class="text-muted mb-md-5">{% block authtext %}{% endblock %}</p>
{% if error %}
<div class="alert alert-danger alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show d-flex my-3" role="alert">
<i class="fas fa-info-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
</div>
<div class="col-12 col-md-6 d-none d-md-block">
<div class="splash-wrapper">
<div class="splash-overlay"></div>
<img alt="cover" loading="lazy" class="splash-img" src="/assets/images/{{SITE_NAME}}/cover.webp?v=1014"></img>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -1,47 +1,47 @@
<div class="modal fade" id="awardModal" tabindex="-1" role="dialog" aria-labelledby="awardModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered awardmodal my-5" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Give Award</h5>
<button class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div id="awardModalBody" class="modal-body mb-3">
<form id="awardTarget" class="pt-3 pb-0" action="" method="post">
<input type="hidden" name="formkey", value="{{v.formkey}}">
<div class="card-columns award-columns awards-wrapper">
{% for award in v.user_awards %}
<a role="button" id="{{award.kind}}" class="card" onclick="pick('{{award.kind}}', {{award.price}}*{{v.discount}} <= {{v.procoins}}, {{award.price}}*{{v.discount}} <= {{v.coins}})">
<i class="{{award.icon}} {{award.color}}"></i>
<div class="pt-2" style="font-weight: bold; font-size: 14px; color:#E1E1E1">{{award.title}}</div>
<div class="text-muted"><span id="{{award.kind}}-owned">{{award.owned}}</span> owned</div>
</a>
{% endfor %}
</div>
<label id="notelabel" for="note" class="pt-4">Note (optional):</label>
<input autocomplete="off" id="kind" name="kind" value="" hidden>
<textarea autocomplete="off" id="note" maxlength="200" name="note" class="form-control" placeholder="Note to include in award notification..."></textarea>
<input autocomplete="off" id="giveaward" class="awardbtn btn btn-primary mt-3" style="float:right" type="submit" value="Give Award" disabled>
<button id="buy1" class="awardbtn btn btn-primary mt-3 mx-3" type="button" disabled style="float:right" onclick="buy(true)">Buy with marseybux</button>
<button id="buy2" class="awardbtn btn btn-primary mt-3" type="button" disabled style="float:right" onclick="buy()">Buy with coins</button>
<pre>
</pre>
</form>
</div>
</div>
</div>
<div class="toast" id="toast-post-success2" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text2">Action successful!</span>
</div>
</div>
<div class="toast" id="toast-post-error2" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text2">Error, please try again later.</span>
</div>
</div>
</div>
<div class="modal fade" id="awardModal" tabindex="-1" role="dialog" aria-labelledby="awardModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered awardmodal my-5" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Give Award</h5>
<button class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div id="awardModalBody" class="modal-body mb-3">
<form id="awardTarget" class="pt-3 pb-0" action="" method="post">
<input type="hidden" name="formkey", value="{{v.formkey}}">
<div class="card-columns award-columns awards-wrapper">
{% for award in v.user_awards %}
<a role="button" id="{{award.kind}}" class="card" onclick="pick('{{award.kind}}', {{award.price}}*{{v.discount}} <= {{v.procoins}}, {{award.price}}*{{v.discount}} <= {{v.coins}})">
<i class="{{award.icon}} {{award.color}}"></i>
<div class="pt-2" style="font-weight: bold; font-size: 14px; color:#E1E1E1">{{award.title}}</div>
<div class="text-muted"><span id="{{award.kind}}-owned">{{award.owned}}</span> owned</div>
</a>
{% endfor %}
</div>
<label id="notelabel" for="note" class="pt-4">Note (optional):</label>
<input autocomplete="off" id="kind" name="kind" value="" hidden>
<textarea autocomplete="off" id="note" maxlength="200" name="note" class="form-control" placeholder="Note to include in award notification..."></textarea>
<input autocomplete="off" id="giveaward" class="awardbtn btn btn-primary mt-3" style="float:right" type="submit" value="Give Award" disabled>
<button id="buy1" class="awardbtn btn btn-primary mt-3 mx-3" type="button" disabled style="float:right" onclick="buy(true)">Buy with marseybux</button>
<button id="buy2" class="awardbtn btn btn-primary mt-3" type="button" disabled style="float:right" onclick="buy()">Buy with coins</button>
<pre>
</pre>
</form>
</div>
</div>
</div>
<div class="toast" id="toast-post-success2" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text2">Action successful!</span>
</div>
</div>
<div class="toast" id="toast-post-error2" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text2">Error, please try again later.</span>
</div>
</div>
</div>
<script src="/assets/js/award_modal.js?v=248" data-cfasync="false"></script>

View file

@ -1,39 +1,39 @@
{% extends "default.html" %}
{% block content %}
<script src="/assets/js/sort_table.js?v=242"></script>
<pre>
</pre>
<h1>User Badges</h1>
<div>This page describes the requirements for obtaining all profile badges.</div>
<pre>
</pre>
<div class="overflow-x-auto">
<table id="sortable_table" class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Image</th>
<th>Description</th>
<th role="button" onclick="sort_table(4)">#</th>
<th role="button" onclick="sort_table(5)">Rarity</th>
</tr>
</thead>
{% for badge in badges %}
<tr>
<td>{{loop.index}}</td>
<td>{{badge.name}}</td>
<td><img alt="{{badge.name}}" loading="lazy" src="/assets/images/badges/{{badge.id}}.webp?v=1016" width=45.83 height=50>
<td>{{badge.description}}</td>
{%- set ct = counts[badge.id] if badge.id in counts else (0, 0) %}
<td class="badges-rarity-qty">{{ ct[0] }}</td>
<td class="badges-rarity-ratio">{{ "{:0.3f}".format(ct[1]) }}%</td>
</tr>
{% endfor %}
</table>
{% extends "default.html" %}
{% block content %}
<script src="/assets/js/sort_table.js?v=242"></script>
<pre>
</pre>
<h1>User Badges</h1>
<div>This page describes the requirements for obtaining all profile badges.</div>
<pre>
</pre>
<div class="overflow-x-auto">
<table id="sortable_table" class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Image</th>
<th>Description</th>
<th role="button" onclick="sort_table(4)">#</th>
<th role="button" onclick="sort_table(5)">Rarity</th>
</tr>
</thead>
{% for badge in badges %}
<tr>
<td>{{loop.index}}</td>
<td>{{badge.name}}</td>
<td><img alt="{{badge.name}}" loading="lazy" src="/assets/images/badges/{{badge.id}}.webp?v=1016" width=45.83 height=50>
<td>{{badge.description}}</td>
{%- set ct = counts[badge.id] if badge.id in counts else (0, 0) %}
<td class="badges-rarity-qty">{{ ct[0] }}</td>
<td class="badges-rarity-ratio">{{ "{:0.3f}".format(ct[1]) }}%</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,38 +1,38 @@
<script src="/assets/js/ban_modal.js?v=241"></script>
<div class="modal fade" id="banModal" tabindex="-1" role="dialog" aria-labelledby="banModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header pt-3">
<h5 id="banModalTitle"></h5>
<button class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div class="modal-body" id="ban-modal-body">
<form id="banModalForm">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="ban-modal-link">Public ban reason (optional)</label>
<textarea autocomplete="off" maxlength="64" name="reason" form="banModalForm" class="form-control" id="ban-modal-link" aria-label="With textarea" placeholder="Enter reason"></textarea>
<label for="days" class="mt-3">Duration days</label>
<input autocomplete="off" type="number" step="any" name="days" id="days" class="form-control" placeholder="leave blank for permanent">
<div class="custom-control custom-switch mt-3">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="alts" name="alts">
<label class="custom-control-label" for="alts">Ban known alts</label>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-link text-muted" data-bs-dismiss="modal">Cancel</button>
<button id="banUserButton" class="btn btn-danger" data-bs-dismiss="modal"></button>
</div>
</div>
</div>
<script src="/assets/js/ban_modal.js?v=241"></script>
<div class="modal fade" id="banModal" tabindex="-1" role="dialog" aria-labelledby="banModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header pt-3">
<h5 id="banModalTitle"></h5>
<button class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div class="modal-body" id="ban-modal-body">
<form id="banModalForm">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<label for="ban-modal-link">Public ban reason (optional)</label>
<textarea autocomplete="off" maxlength="64" name="reason" form="banModalForm" class="form-control" id="ban-modal-link" aria-label="With textarea" placeholder="Enter reason"></textarea>
<label for="days" class="mt-3">Duration days</label>
<input autocomplete="off" type="number" step="any" name="days" id="days" class="form-control" placeholder="leave blank for permanent">
<div class="custom-control custom-switch mt-3">
<input autocomplete="off" type="checkbox" class="custom-control-input" id="alts" name="alts">
<label class="custom-control-label" for="alts">Ban known alts</label>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-link text-muted" data-bs-dismiss="modal">Cancel</button>
<button id="banUserButton" class="btn btn-danger" data-bs-dismiss="modal"></button>
</div>
</div>
</div>
</div>

View file

@ -1,23 +1,23 @@
{% extends "settings2.html" %}
{% block content %}
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Ban reason</th>
<th>Banned by</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}}" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{% if user.ban_reason %}{{user.ban_reason}}{% endif %}</td>
<td href="/@{{user.banned_by.username}}"><img loading="lazy" src="{{user.banned_by.profile_url}}" class="pp20"><span {% if user.banned_by.patron %}class="patron" style="background-color:#{{user.banned_by.namecolor}}"{% endif %}>{{user.banned_by.username}}</span></a></td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% extends "settings2.html" %}
{% block content %}
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Ban reason</th>
<th>Banned by</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}}" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{% if user.ban_reason %}{{user.ban_reason}}{% endif %}</td>
<td href="/@{{user.banned_by.username}}"><img loading="lazy" src="{{user.banned_by.profile_url}}" class="pp20"><span {% if user.banned_by.patron %}class="patron" style="background-color:#{{user.banned_by.namecolor}}"{% endif %}>{{user.banned_by.username}}</span></a></td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,24 +1,24 @@
{% extends "settings2.html" %}
{% block pagetitle %}Blocks{% endblock %}
{% block content %}
<h1> Blocks</h1>
<pre></pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>User</th>
<th>Target</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td>{{loop.index}}</td>
<td><a style="font-weight:bold;color:#{{user.namecolor}}" href="/@{{user.username}}"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td><a style="font-weight:bold;color:#{{targets[loop.index-1].namecolor}}" href="/@{{targets[loop.index-1].username}}"><span {% if targets[loop.index-1].patron %}class="patron" style="background-color:#{{targets[loop.index-1].namecolor}}"{% endif %}>{{targets[loop.index-1].username}}</span></a></td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% extends "settings2.html" %}
{% block pagetitle %}Blocks{% endblock %}
{% block content %}
<h1> Blocks</h1>
<pre></pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>User</th>
<th>Target</th>
</tr>
</thead>
{% for user in users %}
<tr>
<td>{{loop.index}}</td>
<td><a style="font-weight:bold;color:#{{user.namecolor}}" href="/@{{user.username}}"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td><a style="font-weight:bold;color:#{{targets[loop.index-1].namecolor}}" href="/@{{targets[loop.index-1].username}}"><span {% if targets[loop.index-1].patron %}class="patron" style="background-color:#{{targets[loop.index-1].namecolor}}"{% endif %}>{{targets[loop.index-1].username}}</span></a></td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,109 +1,109 @@
{% extends "settings2.html" %}
{% block pagetitle %}Changelog{% endblock %}
{% block desktopBanner %}
<div class="row" style="overflow: visible;padding-top:5px;">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
{% block navbar %}
<div class="font-weight-bold py-3"></div>
<div class="d-flex align-items-center">
<div class="text-small font-weight-bold mr-2"></div>
<div class="dropdown dropdown-actions">
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% if t=="hour" %}<i class="fas fa-clock mr-1"></i>
{% elif t=="day" %}<i class="fas fa-calendar-day mr-1"></i>
{% elif t=="week" %}<i class="fas fa-calendar-week mr-1"></i>
{% elif t=="month" %}<i class="fas fa-calendar-alt mr-1"></i>
{% elif t=="year" %}<i class="fas fa-calendar mr-1"></i>
{% elif t=="all" %}<i class="fas fa-infinity mr-1"></i>
{% endif %}
{{t | capitalize}}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
{% if t != "hour" %}<a class="dropdown-item" href="?sort={{sort}}&t=hour"><i class="fas fa-clock mr-2"></i>Hour</a>{% endif %}
{% if t != "day" %}<a class="dropdown-item" href="?sort={{sort}}&t=day"><i class="fas fa-calendar-day mr-2"></i>Day</a>{% endif %}
{% if t != "week" %}<a class="dropdown-item" href="?sort={{sort}}&t=week"><i class="fas fa-calendar-week mr-2"></i>Week</a>{% endif %}
{% if t != "month" %}<a class="dropdown-item" href="?sort={{sort}}&t=month"><i class="fas fa-calendar-alt mr-2"></i>Month</a>{% endif %}
{% if t != "year" %}<a class="dropdown-item" href="?sort={{sort}}&t=year"><i class="fas fa-calendar mr-2"></i>Year</a>{% endif %}
{% if t != "all" %}<a class="dropdown-item" href="?sort={{sort}}&t=all"><i class="fas fa-infinity mr-2"></i>All</a>{% endif %}
</div>
</div>
<div class="text-small font-weight-bold ml-3 mr-2"></div>
<div class="dropdown dropdown-actions">
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% if sort=="hot" %}<i class="fas fa-fire mr-1"></i>{% endif %}
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
{% if sort=="comments" %}<i class="fas fa-comments mr-1"></i>{% endif %}
{{sort | capitalize}}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton2" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
{% if sort != "hot" %}<a class="dropdown-item" href="?sort=hot&t={{t}}"><i class="fas fa-fire mr-2"></i>Hot</a>{% endif %}
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
{% if sort != "comments" %}<a class="dropdown-item" href="?sort=comments&t={{t}}"><i class="fas fa-comments mr-2"></i>Comments</a>{% endif %}
</div>
</div>
</div>
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
{% if v %}
<a id="subscribe" class="{% if v.changelogsub %}d-none{% endif %} btn btn-primary followbutton" role="button" onclick="post_toast2(this, '/changelogsub','subscribe','unsubscribe')">Subscribe</a>
<a id="unsubscribe" class="{% if not v.changelogsub %}d-none{% endif %} btn btn-primary followbutton" role="button" onclick="post_toast2(this, '/changelogsub','subscribe','unsubscribe')">Unsubscribe</a>
{% endif %}
<div class="row no-gutters {% if listing %}mt-md-3{% elif not listing %}my-md-3{% endif %}">
<div class="col-12">
<div class="posts" id="posts">
{% include "submission_listing.html" %}
</div>
</div>
</div>
{% if listing %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?sort={{sort}}&page={{page-1}}&t={{t}}{% if only %}&only={{only}}{% endif %}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?sort={{sort}}&page={{page+1}}&t={{t}}{% if only %}&only={{only}}{% endif %}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endif %}
<script src="/assets/js/post_toast2.js?v=243"></script>
{% extends "settings2.html" %}
{% block pagetitle %}Changelog{% endblock %}
{% block desktopBanner %}
<div class="row" style="overflow: visible;padding-top:5px;">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
{% block navbar %}
<div class="font-weight-bold py-3"></div>
<div class="d-flex align-items-center">
<div class="text-small font-weight-bold mr-2"></div>
<div class="dropdown dropdown-actions">
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% if t=="hour" %}<i class="fas fa-clock mr-1"></i>
{% elif t=="day" %}<i class="fas fa-calendar-day mr-1"></i>
{% elif t=="week" %}<i class="fas fa-calendar-week mr-1"></i>
{% elif t=="month" %}<i class="fas fa-calendar-alt mr-1"></i>
{% elif t=="year" %}<i class="fas fa-calendar mr-1"></i>
{% elif t=="all" %}<i class="fas fa-infinity mr-1"></i>
{% endif %}
{{t | capitalize}}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
{% if t != "hour" %}<a class="dropdown-item" href="?sort={{sort}}&t=hour"><i class="fas fa-clock mr-2"></i>Hour</a>{% endif %}
{% if t != "day" %}<a class="dropdown-item" href="?sort={{sort}}&t=day"><i class="fas fa-calendar-day mr-2"></i>Day</a>{% endif %}
{% if t != "week" %}<a class="dropdown-item" href="?sort={{sort}}&t=week"><i class="fas fa-calendar-week mr-2"></i>Week</a>{% endif %}
{% if t != "month" %}<a class="dropdown-item" href="?sort={{sort}}&t=month"><i class="fas fa-calendar-alt mr-2"></i>Month</a>{% endif %}
{% if t != "year" %}<a class="dropdown-item" href="?sort={{sort}}&t=year"><i class="fas fa-calendar mr-2"></i>Year</a>{% endif %}
{% if t != "all" %}<a class="dropdown-item" href="?sort={{sort}}&t=all"><i class="fas fa-infinity mr-2"></i>All</a>{% endif %}
</div>
</div>
<div class="text-small font-weight-bold ml-3 mr-2"></div>
<div class="dropdown dropdown-actions">
<button class="btn btn-secondary dropdown-toggle" role="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% if sort=="hot" %}<i class="fas fa-fire mr-1"></i>{% endif %}
{% if sort=="top" %}<i class="fas fa-arrow-alt-circle-up mr-1"></i>{% endif %}
{% if sort=="bottom" %}<i class="fas fa-arrow-alt-circle-down mr-1"></i>{% endif %}
{% if sort=="new" %}<i class="fas fa-sparkles mr-1"></i>{% endif %}
{% if sort=="old" %}<i class="fas fa-book mr-1"></i>{% endif %}
{% if sort=="controversial" %}<i class="fas fa-bullhorn mr-1"></i>{% endif %}
{% if sort=="comments" %}<i class="fas fa-comments mr-1"></i>{% endif %}
{{sort | capitalize}}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton2" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 31px, 0px);">
{% if sort != "hot" %}<a class="dropdown-item" href="?sort=hot&t={{t}}"><i class="fas fa-fire mr-2"></i>Hot</a>{% endif %}
{% if sort != "top" %}<a class="dropdown-item" href="?sort=top&t={{t}}"><i class="fas fa-arrow-alt-circle-up mr-2"></i>Top</a>{% endif %}
{% if sort != "bottom" %}<a class="dropdown-item" href="?sort=bottom&t={{t}}"><i class="fas fa-arrow-alt-circle-down mr-2"></i>Bottom</a>{% endif %}
{% if sort != "new" %}<a class="dropdown-item" href="?sort=new&t={{t}}"><i class="fas fa-sparkles mr-2"></i>New</a>{% endif %}
{% if sort != "old" %}<a class="dropdown-item" href="?sort=old&t={{t}}"><i class="fas fa-book mr-2"></i>Old</a>{% endif %}
{% if sort != "controversial" %}<a class="dropdown-item" href="?sort=controversial&t={{t}}"><i class="fas fa-bullhorn mr-2"></i>Controversial</a>{% endif %}
{% if sort != "comments" %}<a class="dropdown-item" href="?sort=comments&t={{t}}"><i class="fas fa-comments mr-2"></i>Comments</a>{% endif %}
</div>
</div>
</div>
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% block content %}
{% if v %}
<a id="subscribe" class="{% if v.changelogsub %}d-none{% endif %} btn btn-primary followbutton" role="button" onclick="post_toast2(this, '/changelogsub','subscribe','unsubscribe')">Subscribe</a>
<a id="unsubscribe" class="{% if not v.changelogsub %}d-none{% endif %} btn btn-primary followbutton" role="button" onclick="post_toast2(this, '/changelogsub','subscribe','unsubscribe')">Unsubscribe</a>
{% endif %}
<div class="row no-gutters {% if listing %}mt-md-3{% elif not listing %}my-md-3{% endif %}">
<div class="col-12">
<div class="posts" id="posts">
{% include "submission_listing.html" %}
</div>
</div>
</div>
{% if listing %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm mb-0">
{% if page>1 %}
<li class="page-item">
<small><a class="page-link" href="?sort={{sort}}&page={{page-1}}&t={{t}}{% if only %}&only={{only}}{% endif %}" tabindex="-1">Prev</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Prev</span></li>
{% endif %}
{% if next_exists %}
<li class="page-item">
<small><a class="page-link" href="?sort={{sort}}&page={{page+1}}&t={{t}}{% if only %}&only={{only}}{% endif %}">Next</a></small>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endif %}
<script src="/assets/js/post_toast2.js?v=243"></script>
{% endblock %}

File diff suppressed because it is too large Load diff

View file

@ -1,58 +1,58 @@
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}} - Contact</title>
{% endblock %}
{% block content %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<h1 class="article-title">Contact {{SITE_NAME}} Admins</h1>
<p>Use this form to contact {{SITE_NAME}} Admins.</p>
<label class="mt-3">Your Email</label>
<input autocomplete="off" class="form-control" value="{{v.email}}" readonly="readonly" disabled>
<form id="contactform" action="/send_admin" method="post" enctype="multipart/form-data">
<label for="input-message" class="mt-3">Your message</label>
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea autocomplete="off" maxlength="10000" id="input-message" form="contactform" name="message" class="form-control" required></textarea>
<label class="btn btn-secondary format m-0 mt-3" onclick="loadEmojis('input-message')" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji">
<i class="fas fa-smile-beam"></i>
</label>
<label class="btn btn-secondary m-0 mt-3" for="file-upload">
<div id="filename"><i class="far fa-image"></i></div>
<input autocomplete="off" id="file-upload" type="file" name="file" accept="image/*, video/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename','file-upload')" hidden>
</label>
<input type="submit" value="Submit" class="btn btn-primary mt-3">
</form>
<pre>
</pre>
<p>If you can see this line, we haven't been contacted by any law enforcement or governmental organizations in 2022 yet.</p>
<pre>
</pre>
{% include "emoji_modal.html" %}
{% endblock %}
{% extends "default.html" %}
{% block title %}
<title>{{SITE_NAME}} - Contact</title>
{% endblock %}
{% block content %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<h1 class="article-title">Contact {{SITE_NAME}} Admins</h1>
<p>Use this form to contact {{SITE_NAME}} Admins.</p>
<label class="mt-3">Your Email</label>
<input autocomplete="off" class="form-control" value="{{v.email}}" readonly="readonly" disabled>
<form id="contactform" action="/send_admin" method="post" enctype="multipart/form-data">
<label for="input-message" class="mt-3">Your message</label>
<input type="hidden" name="formkey" value="{{v.formkey}}">
<textarea autocomplete="off" maxlength="10000" id="input-message" form="contactform" name="message" class="form-control" required></textarea>
<label class="btn btn-secondary format m-0 mt-3" onclick="loadEmojis('input-message')" aria-hidden="true" data-bs-toggle="modal" data-bs-target="#emojiModal" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Add Emoji">
<i class="fas fa-smile-beam"></i>
</label>
<label class="btn btn-secondary m-0 mt-3" for="file-upload">
<div id="filename"><i class="far fa-image"></i></div>
<input autocomplete="off" id="file-upload" type="file" name="file" accept="image/*, video/*" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} onchange="changename('filename','file-upload')" hidden>
</label>
<input type="submit" value="Submit" class="btn btn-primary mt-3">
</form>
<pre>
</pre>
<p>If you can see this line, we haven't been contacted by any law enforcement or governmental organizations in 2022 yet.</p>
<pre>
</pre>
{% include "emoji_modal.html" %}
{% endblock %}

View file

@ -1,332 +1,332 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval' ajax.cloudflare.com; connect-src 'self' tls-use1.fpapi.io api.fpjs.io {% if PUSHER_ID != 'blahblahblah' %}{{PUSHER_ID}}.pushnotifications.pusher.com{% endif %}; object-src 'none';">
<script src="/assets/js/bootstrap.js?v=245"></script>
{% if v %}
<style>:root{--primary:#{{v.themecolor}}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=250">
<link rel="stylesheet" href="/assets/css/{{v.theme}}.css?v=56">
{% if v.agendaposter %}
<style>
html {
cursor:url('/assets/images/dildo.webp?v=1008'), auto;
}
.nav-item .text-small.font-weight-bold::before {
content: "((("
}
.nav-item .text-small.font-weight-bold::after {
content: ")))"
}
.nav-item .text-small-extra.text-primary {
font-size: 0 !important
}
.nav-item .text-small-extra.text-primary i {
font-size: 11px !important
}
</style>
{% elif v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=250">
<link rel="stylesheet" href="/assets/css/{{config('DEFAULT_THEME')}}.css?v=56">
{% endif %}
{% if request.path == '/catalog' %}
<link rel="stylesheet" href="/assets/css/catalog.css?v=1">
{% endif %}
{% if sub and sub.css and not request.path.endswith('settings') %}
<link rel="stylesheet" href="/h/{{sub.name}}/css" type="text/css">
{% endif %}
{% if v and v.themecolor == '30409f' %}
<style>
p a {
color: #2a96f3;
}
</style>
{% endif %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="thumbnail" content="/assets/images/{{SITE_NAME}}/site_preview.webp?v=1015">
<link rel="icon" type="image/png" href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015">
{% block title %}
<title>{{SITE_NAME}}</title>
<meta property="og:type" content="article">
<meta property="og:title" content="{{SITE_NAME}}">
<meta property="og:site_name" content="{{request.host}}">
<meta property="og:image" content="/assets/images/{{SITE_NAME}}/site_preview.webp?v=1015">
<meta property="og:url" content="{{SITE_FULL}}{{request.full_path}}">
<meta property="og:description" name="description" content="{{SITE_NAME}} - {{config('DESCRIPTION')}}">
<meta property="og:author" name="author" content="{{SITE_FULL}}">
<meta property="og:site_name" content="{{request.host}}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="{{SITE_FULL}}">
<meta name="twitter:title" content="{{SITE_NAME}}">
<meta name="twitter:creator" content="{{SITE_FULL}}">
<meta name="twitter:description" content="{{SITE_NAME}} - {{config('DESCRIPTION')}}">
<meta name="twitter:image" content="/assets/images/{{SITE_NAME}}/site_preview.webp?v=1015">
<meta name="twitter:url" content="{{SITE_FULL}}{{request.full_path}}">
{% endblock %}
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015">
<link rel="manifest" href="/assets/manifest_{{SITE_NAME}}.json?v=1">
<link rel="mask-icon" href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015">
<link rel="shortcut icon" href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015">
<meta name="apple-mobile-web-app-title" content="{{SITE_NAME}}">
<meta name="application-name" content="{{SITE_NAME}}">
<meta name="msapplication-TileColor" content="#{{config('DEFAULT_COLOR')}}">
<meta name="msapplication-config" content="/assets/browserconfig.xml?v=2">
<meta name="theme-color" content="#{{config('DEFAULT_COLOR')}}">
<link
rel="apple-touch-startup-image"
sizes="320x480"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="640x960"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-icon"
sizes="640x1136"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-icon"
sizes="750x1334"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="768x1004"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="768x1024"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="828x1792"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1024x748"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1024x768"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1125x2436"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1242x2208"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1242x2688"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1334x750"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1536x2008"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1536x2048"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1668x2224"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1792x828"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2048x1496"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2048x1536"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2048x2732"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2208x1242"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2224x1668"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2436x1125"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2668x1242"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2737x2048"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
{% block fixedMobileBarJS %}
{% endblock %}
</head>
<body id="{% if request.path != '/comments' %}{% block pagetype %}frontpage{% endblock %}{% endif %}" {% if v and v.background %}style="{% if path != '/formatting' %}overflow-x: hidden; {% endif %} background:url(/assets/images/backgrounds/{{v.background}}?v=3) center center fixed; background-color: var(--background){% if 'anime' not in v.background %};background-size: cover{% endif %}"{% endif %}>
{% block Banner %}
{% if '@' not in request.path %}
{% if sub %}
<img alt="/h/{{sub.name}} banner" role="button" data-bs-toggle="modal" data-bs-target="#expandImageModal" onclick="expandDesktopImage('{{sub.banner_url}}')" loading="lazy" src="{{sub.banner_url}}" width=100% style="object-fit:cover;max-height:25vw">
{% else %}
<a href="/">
<img alt="site banner" src="/assets/images/{{SITE_NAME}}/banner.webp?v=1046" width="100%">
</a>
{% endif %}
{% endif %}
{% endblock %}
{% include "header.html" %}
{% block mobileUserBanner %}
{% endblock %}
{% block mobileBanner %}
{% endblock %}
{% block postNav %}
{% endblock %}
<div class="container">
<div class="row justify-content-around" id="main-content-row">
<div class="col h-100 {% block customPadding %}{% if request.path.startswith('/@') %}user-gutters{% else %}custom-gutters{% endif %}{% endblock %}" id="main-content-col">
{% block desktopUserBanner %}
{% endblock %}
{% block desktopBanner %}
{% endblock %}
{% block PseudoSubmitForm %}
{% endblock %}
{% block searchText %}
{% endblock %}
{% block content %}
{% endblock %}
{% block pagenav %}
{% endblock %}
</div>
{% block sidebar %}
{% if home or sub and p %}
{% include "sidebar_" + SITE_NAME + ".html" %}
{% endif %}
{% endblock %}
</div>
</div>
{% block mobilenavbar %}
{% include "mobile_navigation_bar.html" %}
{% endblock %}
{% block actionsModal %}
{% endblock %}
{% block reportCommentModal %}
{% endblock %}
{% block GIFtoast %}
{% endblock %}
{% block GIFpicker %}
{% endblock %}
<div class="toast clipboard" id="toast-success" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body text-center">
<i class="fas fa-check-circle text-success mr-2"></i>Link copied to clipboard
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text">Action successful!</span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
<script src="/assets/js/lozad.js?v=242"></script>
{% if v %}
<script src="/assets/js/post_toast2.js?v=243"></script>
<script src="/assets/js/formatting.js?v=240"></script>
{% endif %}
<script src="/assets/js/lite-youtube.js?v=240"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval' ajax.cloudflare.com; connect-src 'self' tls-use1.fpapi.io api.fpjs.io {% if PUSHER_ID != 'blahblahblah' %}{{PUSHER_ID}}.pushnotifications.pusher.com{% endif %}; object-src 'none';">
<script src="/assets/js/bootstrap.js?v=245"></script>
{% if v %}
<style>:root{--primary:#{{v.themecolor}}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=250">
<link rel="stylesheet" href="/assets/css/{{v.theme}}.css?v=56">
{% if v.agendaposter %}
<style>
html {
cursor:url('/assets/images/dildo.webp?v=1008'), auto;
}
.nav-item .text-small.font-weight-bold::before {
content: "((("
}
.nav-item .text-small.font-weight-bold::after {
content: ")))"
}
.nav-item .text-small-extra.text-primary {
font-size: 0 !important
}
.nav-item .text-small-extra.text-primary i {
font-size: 11px !important
}
</style>
{% elif v.css %}
<link rel="stylesheet" href="/@{{v.username}}/css">
{% endif %}
{% else %}
<style>:root{--primary:#{{config('DEFAULT_COLOR')}}</style>
<link rel="stylesheet" href="/assets/css/main.css?v=250">
<link rel="stylesheet" href="/assets/css/{{config('DEFAULT_THEME')}}.css?v=56">
{% endif %}
{% if request.path == '/catalog' %}
<link rel="stylesheet" href="/assets/css/catalog.css?v=1">
{% endif %}
{% if sub and sub.css and not request.path.endswith('settings') %}
<link rel="stylesheet" href="/h/{{sub.name}}/css" type="text/css">
{% endif %}
{% if v and v.themecolor == '30409f' %}
<style>
p a {
color: #2a96f3;
}
</style>
{% endif %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="thumbnail" content="/assets/images/{{SITE_NAME}}/site_preview.webp?v=1015">
<link rel="icon" type="image/png" href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015">
{% block title %}
<title>{{SITE_NAME}}</title>
<meta property="og:type" content="article">
<meta property="og:title" content="{{SITE_NAME}}">
<meta property="og:site_name" content="{{request.host}}">
<meta property="og:image" content="/assets/images/{{SITE_NAME}}/site_preview.webp?v=1015">
<meta property="og:url" content="{{SITE_FULL}}{{request.full_path}}">
<meta property="og:description" name="description" content="{{SITE_NAME}} - {{config('DESCRIPTION')}}">
<meta property="og:author" name="author" content="{{SITE_FULL}}">
<meta property="og:site_name" content="{{request.host}}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="{{SITE_FULL}}">
<meta name="twitter:title" content="{{SITE_NAME}}">
<meta name="twitter:creator" content="{{SITE_FULL}}">
<meta name="twitter:description" content="{{SITE_NAME}} - {{config('DESCRIPTION')}}">
<meta name="twitter:image" content="/assets/images/{{SITE_NAME}}/site_preview.webp?v=1015">
<meta name="twitter:url" content="{{SITE_FULL}}{{request.full_path}}">
{% endblock %}
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-touch-fullscreen" content="yes">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015">
<link rel="manifest" href="/assets/manifest_{{SITE_NAME}}.json?v=1">
<link rel="mask-icon" href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015">
<link rel="shortcut icon" href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015">
<meta name="apple-mobile-web-app-title" content="{{SITE_NAME}}">
<meta name="application-name" content="{{SITE_NAME}}">
<meta name="msapplication-TileColor" content="#{{config('DEFAULT_COLOR')}}">
<meta name="msapplication-config" content="/assets/browserconfig.xml?v=2">
<meta name="theme-color" content="#{{config('DEFAULT_COLOR')}}">
<link
rel="apple-touch-startup-image"
sizes="320x480"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="640x960"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-icon"
sizes="640x1136"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-icon"
sizes="750x1334"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="768x1004"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="768x1024"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="828x1792"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1024x748"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1024x768"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1125x2436"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1242x2208"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1242x2688"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1334x750"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1536x2008"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1536x2048"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1668x2224"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="1792x828"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2048x1496"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2048x1536"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2048x2732"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2208x1242"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2224x1668"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2436x1125"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2668x1242"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
<link
rel="apple-touch-startup-image"
sizes="2737x2048"
href="/assets/images/{{SITE_NAME}}/icon.webp?v=1015"
>
{% block fixedMobileBarJS %}
{% endblock %}
</head>
<body id="{% if request.path != '/comments' %}{% block pagetype %}frontpage{% endblock %}{% endif %}" {% if v and v.background %}style="{% if path != '/formatting' %}overflow-x: hidden; {% endif %} background:url(/assets/images/backgrounds/{{v.background}}?v=3) center center fixed; background-color: var(--background){% if 'anime' not in v.background %};background-size: cover{% endif %}"{% endif %}>
{% block Banner %}
{% if '@' not in request.path %}
{% if sub %}
<img alt="/h/{{sub.name}} banner" role="button" data-bs-toggle="modal" data-bs-target="#expandImageModal" onclick="expandDesktopImage('{{sub.banner_url}}')" loading="lazy" src="{{sub.banner_url}}" width=100% style="object-fit:cover;max-height:25vw">
{% else %}
<a href="/">
<img alt="site banner" src="/assets/images/{{SITE_NAME}}/banner.webp?v=1046" width="100%">
</a>
{% endif %}
{% endif %}
{% endblock %}
{% include "header.html" %}
{% block mobileUserBanner %}
{% endblock %}
{% block mobileBanner %}
{% endblock %}
{% block postNav %}
{% endblock %}
<div class="container">
<div class="row justify-content-around" id="main-content-row">
<div class="col h-100 {% block customPadding %}{% if request.path.startswith('/@') %}user-gutters{% else %}custom-gutters{% endif %}{% endblock %}" id="main-content-col">
{% block desktopUserBanner %}
{% endblock %}
{% block desktopBanner %}
{% endblock %}
{% block PseudoSubmitForm %}
{% endblock %}
{% block searchText %}
{% endblock %}
{% block content %}
{% endblock %}
{% block pagenav %}
{% endblock %}
</div>
{% block sidebar %}
{% if home or sub and p %}
{% include "sidebar_" + SITE_NAME + ".html" %}
{% endif %}
{% endblock %}
</div>
</div>
{% block mobilenavbar %}
{% include "mobile_navigation_bar.html" %}
{% endblock %}
{% block actionsModal %}
{% endblock %}
{% block reportCommentModal %}
{% endblock %}
{% block GIFtoast %}
{% endblock %}
{% block GIFpicker %}
{% endblock %}
<div class="toast clipboard" id="toast-success" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body text-center">
<i class="fas fa-check-circle text-success mr-2"></i>Link copied to clipboard
</div>
</div>
<div class="toast" id="toast-post-success" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-success text-center text-white">
<i class="fas fa-comment-alt-smile mr-2"></i><span id="toast-post-success-text">Action successful!</span>
</div>
</div>
<div class="toast" id="toast-post-error" style="position: fixed; bottom: 1.5rem; margin: 0 auto; left: 0; right: 0; width: 275px; z-index: 1000" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-body bg-danger text-center text-white">
<i class="fas fa-exclamation-circle mr-2"></i><span id="toast-post-error-text">Error, please try again later.</span>
</div>
</div>
<script src="/assets/js/lozad.js?v=242"></script>
{% if v %}
<script src="/assets/js/post_toast2.js?v=243"></script>
<script src="/assets/js/formatting.js?v=240"></script>
{% endif %}
<script src="/assets/js/lite-youtube.js?v=240"></script>
</body>
</html>

View file

@ -1,30 +1,30 @@
<script src="/assets/js/delete_post_modal.js?v=240"></script>
<div class="modal fade" id="deletePostModal" tabindex="-1" role="dialog" aria-labelledby="deletePostModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header d-none d-md-flex">
<h5 class="modal-title">Delete post?</h5>
<button class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div class="modal-body text-center">
<div class="py-4">
<i class="fas fa-trash-alt text-muted d-none d-md-block" style="font-size: 3.5rem;"></i>
</div>
<div class="h4 d-md-none">Delete post?</div>
<p class="d-none d-md-block">Your post will be deleted everywhere on {{SITE_NAME}}.</p>
<p class="text-muted d-md-none">Your post will be deleted everywhere on {{SITE_NAME}}.</p>
<button id="deletePostButton" class="btn btn-danger btn-block mt-5" data-bs-dismiss="modal">Delete post</button>
<button class="btn btn-secondary btn-block" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<script src="/assets/js/delete_post_modal.js?v=240"></script>
<div class="modal fade" id="deletePostModal" tabindex="-1" role="dialog" aria-labelledby="deletePostModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header d-none d-md-flex">
<h5 class="modal-title">Delete post?</h5>
<button class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
<div class="modal-body text-center">
<div class="py-4">
<i class="fas fa-trash-alt text-muted d-none d-md-block" style="font-size: 3.5rem;"></i>
</div>
<div class="h4 d-md-none">Delete post?</div>
<p class="d-none d-md-block">Your post will be deleted everywhere on {{SITE_NAME}}.</p>
<p class="text-muted d-md-none">Your post will be deleted everywhere on {{SITE_NAME}}.</p>
<button id="deletePostButton" class="btn btn-danger btn-block mt-5" data-bs-dismiss="modal">Delete post</button>
<button class="btn btn-secondary btn-block" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>

View file

@ -1,32 +1,32 @@
{% extends "email/default.html" %}
{% block title %}Remove Two-Factor Authentication{% endblock %}</h1>
{% block preheader %}Remove Two-Factor Authentication.{% endblock %}
{% block content %}
<p>We received a request to remove two-factor authentication from your account. In 72 hours, click the link below.</p>
<p>If you didn't make this request, change your password and use the Log Out Everywhere feature in your <a href="/settings/security">Security Settings</a> to permanently invalidate the link.</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Remove 2FA</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_NAME}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}
{% extends "email/default.html" %}
{% block title %}Remove Two-Factor Authentication{% endblock %}</h1>
{% block preheader %}Remove Two-Factor Authentication.{% endblock %}
{% block content %}
<p>We received a request to remove two-factor authentication from your account. In 72 hours, click the link below.</p>
<p>If you didn't make this request, change your password and use the Log Out Everywhere feature in your <a href="/settings/security">Security Settings</a> to permanently invalidate the link.</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Remove 2FA</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_NAME}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}

View file

@ -1,410 +1,410 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
html {
font-size: 14px;
}
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #FF66AC!important;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
body,
td,
th {
font-family: Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #121213;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #121213;
font-size: 1rem;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #121213;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 1rem;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 1rem;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.button {
background-color: #FF66AC;
border-top: 10px solid #FF66AC;
border-right: 18px solid #FF66AC;
border-bottom: 10px solid #FF66AC;
border-left: 18px solid #FF66AC;
display: inline-block;
color: #FFF!important;
text-decoration: none;
border-radius: .25rem;
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #23CE6B;
border-top: 10px solid #23CE6B;
border-right: 18px solid #23CE6B;
border-bottom: 10px solid #23CE6B;
border-left: 18px solid #23CE6B;
}
.button--red {
background-color: #F05D5E;
border-top: 10px solid #F05D5E;
border-right: 18px solid #F05D5E;
border-bottom: 10px solid #F05D5E;
border-left: 18px solid #F05D5E;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #EDF2F7;
padding: 1rem;
border-radius: 0.35rem;
}
.attributes_item {
padding: 0;
}
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #EDF2F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #121213;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #E6E6E6;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #E6E6E6;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #121213;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #EDF2F7;
color: #121213;
}
p {
color: #121213;
}
p.sub {
color: #6B6E76;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #EDF2F7;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-masthead {
display: none;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 1.25rem;
font-weight: bold;
color: #121213;
text-decoration: none;
}
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #cfcfcf;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #cfcfcf;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #6B6E76;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #E6E6E6;
}
.content-cell {
padding: 35px;
}
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #121213 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3 {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
</style>
</head>
<body>
<span class="preheader">{% block preheader %}Thanks for joining {{SITE_NAME}}! Please take a sec to verify the email you used to sign up.{% endblock %}</span>
<div class="overflow-x-auto"><table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto"><table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="/" class="f-fallback email-masthead_name">
{{SITE_NAME}}
</a>
</td>
</tr>
<tr>
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
<div class="overflow-x-auto"><table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>{% block title %}Title Goes Here{% endblock %}</h1>
{% block content %}
{% for entry in data %}
<h3>{{entry[0]}}</h3>
<p>{{entry[1]}}</p>
{% endfor %}
{% endblock %}
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta name="description" content="{{config('DESCRIPTION')}}">
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
html {
font-size: 14px;
}
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #FF66AC!important;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
body,
td,
th {
font-family: Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #121213;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #121213;
font-size: 1rem;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #121213;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 1rem;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 1rem;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.button {
background-color: #FF66AC;
border-top: 10px solid #FF66AC;
border-right: 18px solid #FF66AC;
border-bottom: 10px solid #FF66AC;
border-left: 18px solid #FF66AC;
display: inline-block;
color: #FFF!important;
text-decoration: none;
border-radius: .25rem;
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #23CE6B;
border-top: 10px solid #23CE6B;
border-right: 18px solid #23CE6B;
border-bottom: 10px solid #23CE6B;
border-left: 18px solid #23CE6B;
}
.button--red {
background-color: #F05D5E;
border-top: 10px solid #F05D5E;
border-right: 18px solid #F05D5E;
border-bottom: 10px solid #F05D5E;
border-left: 18px solid #F05D5E;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #EDF2F7;
padding: 1rem;
border-radius: 0.35rem;
}
.attributes_item {
padding: 0;
}
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #EDF2F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #121213;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #E6E6E6;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #E6E6E6;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #121213;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #EDF2F7;
color: #121213;
}
p {
color: #121213;
}
p.sub {
color: #6B6E76;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #EDF2F7;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-masthead {
display: none;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 1.25rem;
font-weight: bold;
color: #121213;
text-decoration: none;
}
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #cfcfcf;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #cfcfcf;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #6B6E76;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #E6E6E6;
}
.content-cell {
padding: 35px;
}
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #121213 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3 {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
</style>
</head>
<body>
<span class="preheader">{% block preheader %}Thanks for joining {{SITE_NAME}}! Please take a sec to verify the email you used to sign up.{% endblock %}</span>
<div class="overflow-x-auto"><table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto"><table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="/" class="f-fallback email-masthead_name">
{{SITE_NAME}}
</a>
</td>
</tr>
<tr>
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
<div class="overflow-x-auto"><table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>{% block title %}Title Goes Here{% endblock %}</h1>
{% block content %}
{% for entry in data %}
<h3>{{entry[0]}}</h3>
<p>{{entry[1]}}</p>
{% endfor %}
{% endblock %}
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -1,54 +1,54 @@
{% extends "email/default.html" %}
{% block title %}Verify Your Email{% endblock %}</h1>
{% block preheader %}Verify your new {{SITE_NAME}} email.{% endblock %}
{% block content %}
<p>You told us you wanted to change your {{SITE_NAME}} account email. To finish this process, please verify your new email address:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your current information:</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_NAME}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}
{% extends "email/default.html" %}
{% block title %}Verify Your Email{% endblock %}</h1>
{% block preheader %}Verify your new {{SITE_NAME}} email.{% endblock %}
{% block content %}
<p>You told us you wanted to change your {{SITE_NAME}} account email. To finish this process, please verify your new email address:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your current information:</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_NAME}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}

View file

@ -1,52 +1,52 @@
{% extends "email/default.html" %}
{% block title %}Welcome to {{SITE_NAME}}!{% endblock %}</h1>
{% block content %}
<p>Thanks for joining {{SITE_NAME}}. Were happy to have you on board. To get the most out of {{SITE_NAME}}, please verify your account email:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your username.</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_NAME}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}
{% extends "email/default.html" %}
{% block title %}Welcome to {{SITE_NAME}}!{% endblock %}</h1>
{% block content %}
<p>Thanks for joining {{SITE_NAME}}. Were happy to have you on board. To get the most out of {{SITE_NAME}}, please verify your account email:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Verify email</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your username.</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>Please note that {{SITE_NAME}} will never ask you for your email, password, or two-factor token via email, text, or phone.</p>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}

View file

@ -1,52 +1,52 @@
{% extends "email/default.html" %}
{% block title %}Reset Your Password{% endblock %}
{% block preheader %}Reset your {{SITE_NAME}} password.{% endblock %}
{% block content %}
<p>To reset your password, click the button below:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Reset password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your login information:</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}
{% extends "email/default.html" %}
{% block title %}Reset Your Password{% endblock %}
{% block preheader %}Reset your {{SITE_NAME}} password.{% endblock %}
{% block content %}
<p>To reset your password, click the button below:</p>
<div class="overflow-x-auto"><table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<div class="overflow-x-auto><table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{action_url}}" class="f-fallback button" target="_blank">Reset password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>For reference, here's your login information:</p>
<div class="overflow-x-auto"><table class="attributes" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_content">
<div class="overflow-x-auto><table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Email:</strong> {{v.email}}
</span>
</td>
</tr>
<tr>
<td class="attributes_item">
<span class="f-fallback">
<strong>Username:</strong> {{v.username}}
</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div class="overflow-x-auto"><table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{action_url}}</p>
</td>
</tr>
</table>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show more