Convert text file line endings to LF.
This commit is contained in:
parent
67d5db425f
commit
1ee9c1bfa7
166 changed files with 33162 additions and 33162 deletions
8
.gitattributes
vendored
8
.gitattributes
vendored
|
@ -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
|
||||
|
|
34
Dockerfile
34
Dockerfile
|
@ -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" ]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
|
@ -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 *
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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"
|
||||
});
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>';
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
User-agent: *
|
||||
User-agent: *
|
||||
Allow: /
|
|
@ -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 *
|
|
@ -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})>"
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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")
|
|
@ -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
|
@ -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})>"
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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\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['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('&','&')).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('&','&')
|
||||
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("&", "&").replace('<','<').replace('>','>').replace('"', '"').replace("'", "'").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\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['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('&','&')).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('&','&')
|
||||
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("&", "&").replace('<','<').replace('>','>').replace('"', '"').replace("'", "'").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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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\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\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.")
|
||||
|
|
|
@ -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
|
@ -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}")
|
|
@ -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('/')
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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!"}
|
|
@ -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
|
@ -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
|
@ -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
|
|
@ -1,84 +1,84 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>{{SITE_NAME}}</title>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<pre></pre>
|
||||
<pre></pre>
|
||||
<h3> 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> 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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 you’re 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 you’re 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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 you’re 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 you’re 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 %}
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
{% extends "email/default.html" %}
|
||||
|
||||
{% block title %}Welcome to {{SITE_NAME}}!{% endblock %}</h1>
|
||||
|
||||
{% block content %}
|
||||
<p>Thanks for joining {{SITE_NAME}}. We’re 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 you’re 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}}. We’re 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 you’re 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 %}
|
||||
|
|
|
@ -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 you’re 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 you’re 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
Loading…
Add table
Add a link
Reference in a new issue