From a62beb7a48044686e50a27a53d112a99f8607461 Mon Sep 17 00:00:00 2001
From: citrons <citrons@mondecitronne.com>
Date: Sat, 14 Jun 2025 01:03:33 -0500
Subject: convert spaces to tabs

---
 apioforum/__init__.py                     | 140 ++---
 apioforum/admin.py                        |   8 +-
 apioforum/auth.py                         | 186 +++----
 apioforum/csscolors.py                    | 296 +++++-----
 apioforum/db.py                           | 238 ++++-----
 apioforum/forum.py                        | 860 +++++++++++++++---------------
 apioforum/fuzzy.py                        |  50 +-
 apioforum/mdrender.py                     |  62 +--
 apioforum/permissions.py                  |  48 +-
 apioforum/read.py                         |  16 +-
 apioforum/roles.py                        | 154 +++---
 apioforum/static/style.css                | 112 ++--
 apioforum/templates/admin/admin_page.html |   6 +-
 apioforum/templates/auth/login.html       |  22 +-
 apioforum/templates/auth/register.html    |  22 +-
 apioforum/templates/base.html             |  48 +-
 apioforum/templates/common.html           |  50 +-
 apioforum/templates/config_thread.html    |  22 +-
 apioforum/templates/create_thread.html    |  12 +-
 apioforum/templates/edit_forum.html       |   6 +-
 apioforum/templates/search_results.html   |  34 +-
 apioforum/templates/view_forum.html       |   4 +-
 apioforum/templates/view_thread.html      | 116 ++--
 apioforum/thread.py                       | 746 +++++++++++++-------------
 apioforum/user.py                         | 122 ++---
 apioforum/util.py                         |  18 +-
 apioforum/webhooks.py                     | 342 ++++++------
 27 files changed, 1870 insertions(+), 1870 deletions(-)

diff --git a/apioforum/__init__.py b/apioforum/__init__.py
index a7d70c0..8a917c9 100644
--- a/apioforum/__init__.py
+++ b/apioforum/__init__.py
@@ -6,73 +6,73 @@ from .db import get_db
 import os
 
 def create_app():
-    app = Flask(__name__, instance_relative_config=True)
-    app.config.from_mapping(
-        SECRET_KEY="dev",
-        DATABASE=os.path.join(app.instance_path, 'database.db'),
-    )
-    app.config.from_pyfile("config.py",silent=True)
-    try:
-        os.makedirs(app.instance_path)
-    except OSError:
-        pass
-
-    app.jinja_env.trim_blocks = True
-    app.jinja_env.lstrip_blocks = True
-
-    from . import db
-    db.init_app(app)
-    from . import permissions
-    permissions.init_app(app)
-
-    from . import auth
-    app.register_blueprint(auth.bp)
-
-    from . import forum
-    app.register_blueprint(forum.bp)
-
-    from . import thread
-    app.register_blueprint(thread.bp)
-
-    from . import admin
-    app.register_blueprint(admin.bp)
-
-    from . import user
-    app.register_blueprint(user.bp)
-
-    from .fuzzy import fuzzy
-    app.jinja_env.filters['fuzzy']=fuzzy
-
-    from .util import gen_colour
-    app.jinja_env.filters['gen_colour']=gen_colour
-
-    @app.context_processor
-    def path_for_next():
-        p = request.path
-        if len(request.query_string) > 0 and not p.startswith("/auth"):
-            p += "?" + request.query_string.decode("utf-8")
-        return dict(path_for_next=p)
-
-    app.jinja_env.globals.update(forum_path=forum.forum_path)
-    app.jinja_env.globals.update(post_jump=thread.post_jump)
-    from .roles import has_permission, is_bureaucrat, get_user_role
-    app.jinja_env.globals.update(
-            has_permission=has_permission,
-            is_bureaucrat=is_bureaucrat,
-            get_user_role=get_user_role)
-
-    from .mdrender import render
-    @app.template_filter('md')
-    def md_render(s):
-        return render(s)
-
-    @app.errorhandler(404)
-    def not_found(e):
-        return render_template('err/404.html'), 404
-    @app.errorhandler(403)
-    def forbidden(e):
-        return render_template('err/403.html'), 403
-
-    app.add_url_rule("/",endpoint="index")
-
-    return app
+	app = Flask(__name__, instance_relative_config=True)
+	app.config.from_mapping(
+		SECRET_KEY="dev",
+		DATABASE=os.path.join(app.instance_path, 'database.db'),
+	)
+	app.config.from_pyfile("config.py",silent=True)
+	try:
+		os.makedirs(app.instance_path)
+	except OSError:
+		pass
+
+	app.jinja_env.trim_blocks = True
+	app.jinja_env.lstrip_blocks = True
+
+	from . import db
+	db.init_app(app)
+	from . import permissions
+	permissions.init_app(app)
+
+	from . import auth
+	app.register_blueprint(auth.bp)
+
+	from . import forum
+	app.register_blueprint(forum.bp)
+
+	from . import thread
+	app.register_blueprint(thread.bp)
+
+	from . import admin
+	app.register_blueprint(admin.bp)
+
+	from . import user
+	app.register_blueprint(user.bp)
+
+	from .fuzzy import fuzzy
+	app.jinja_env.filters['fuzzy']=fuzzy
+
+	from .util import gen_colour
+	app.jinja_env.filters['gen_colour']=gen_colour
+
+	@app.context_processor
+	def path_for_next():
+		p = request.path
+		if len(request.query_string) > 0 and not p.startswith("/auth"):
+			p += "?" + request.query_string.decode("utf-8")
+		return dict(path_for_next=p)
+
+	app.jinja_env.globals.update(forum_path=forum.forum_path)
+	app.jinja_env.globals.update(post_jump=thread.post_jump)
+	from .roles import has_permission, is_bureaucrat, get_user_role
+	app.jinja_env.globals.update(
+			has_permission=has_permission,
+			is_bureaucrat=is_bureaucrat,
+			get_user_role=get_user_role)
+
+	from .mdrender import render
+	@app.template_filter('md')
+	def md_render(s):
+		return render(s)
+
+	@app.errorhandler(404)
+	def not_found(e):
+		return render_template('err/404.html'), 404
+	@app.errorhandler(403)
+	def forbidden(e):
+		return render_template('err/403.html'), 403
+
+	app.add_url_rule("/",endpoint="index")
+
+	return app
diff --git a/apioforum/admin.py b/apioforum/admin.py
index b11b735..f96b0c8 100644
--- a/apioforum/admin.py
+++ b/apioforum/admin.py
@@ -1,5 +1,5 @@
 from flask import (
-    Blueprint, render_template
+	Blueprint, render_template
 )
 from .db import get_db
 from .permissions import admin_required
@@ -9,6 +9,6 @@ bp = Blueprint("admin",__name__,url_prefix="/admin")
 @bp.route("/")
 @admin_required
 def admin_page():
-    db = get_db()
-    admins = db.execute("select * from users where admin > 0;").fetchall()
-    return render_template("admin/admin_page.html",admins=admins)
+	db = get_db()
+	admins = db.execute("select * from users where admin > 0;").fetchall()
+	return render_template("admin/admin_page.html",admins=admins)
diff --git a/apioforum/auth.py b/apioforum/auth.py
index 8864fc0..93aafdd 100644
--- a/apioforum/auth.py
+++ b/apioforum/auth.py
@@ -1,6 +1,6 @@
 from flask import (
-    Blueprint, session, request, url_for, render_template, redirect,
-    flash, g
+	Blueprint, session, request, url_for, render_template, redirect,
+	flash, g
 )
 from werkzeug.security import check_password_hash, generate_password_hash
 from .db import get_db
@@ -9,121 +9,121 @@ import functools
 bp = Blueprint("auth", __name__, url_prefix="/auth")
 
 def get_next():
-    return request.args.get('next',url_for('index'))
+	return request.args.get('next',url_for('index'))
 
 @bp.route("/login",methods=('GET','POST'))
 def login():
-    if request.method == "POST":
-        username = request.form["username"]
-        password = request.form["password"]
-        db = get_db()
-        err = None
-        user = db.execute(
-            "SELECT password FROM users WHERE username = ?;",(username,)
-        ).fetchone()
-        if not username:
-            err = "username required"
-        elif not password:
-            err = "password required"
-        elif user is None or not check_password_hash(user['password'], password):
-            err = "invalid login"
+	if request.method == "POST":
+		username = request.form["username"]
+		password = request.form["password"]
+		db = get_db()
+		err = None
+		user = db.execute(
+			"SELECT password FROM users WHERE username = ?;",(username,)
+		).fetchone()
+		if not username:
+			err = "username required"
+		elif not password:
+			err = "password required"
+		elif user is None or not check_password_hash(user['password'], password):
+			err = "invalid login"
 
-        if err is None:
-            session.clear()
-            session['user'] = username
-            if 'keep_logged_in' in request.form:
-                session['keep_logged_in']=True
-                session.permanent = True
-            flash("logged in successfully")
-            return redirect(get_next())
+		if err is None:
+			session.clear()
+			session['user'] = username
+			if 'keep_logged_in' in request.form:
+				session['keep_logged_in']=True
+				session.permanent = True
+			flash("logged in successfully")
+			return redirect(get_next())
 
-        flash(err)
-        
-    return render_template("auth/login.html")
+		flash(err)
+		
+	return render_template("auth/login.html")
 
 
 @bp.route("/register", methods=("GET","POST"))
 def register():
-    if request.method == "POST":
-        username = request.form["username"]
-        password = request.form["password"]
-        db = get_db()
-        err = None
-        if not username:
-            err = "Username required"
-        elif not password:
-            err = "Password required"
-        elif db.execute(
-            "SELECT 1 FROM users WHERE username = ?;", (username,)
-        ).fetchone() is not None:
-            err = f"User {username} is already registered."
-        elif len(username) > 20:
-            err = "username can't be longer than 20 characters"
-        elif not username.isalnum():
-            err = "username must be alphanumeric"
+	if request.method == "POST":
+		username = request.form["username"]
+		password = request.form["password"]
+		db = get_db()
+		err = None
+		if not username:
+			err = "Username required"
+		elif not password:
+			err = "Password required"
+		elif db.execute(
+			"SELECT 1 FROM users WHERE username = ?;", (username,)
+		).fetchone() is not None:
+			err = f"User {username} is already registered."
+		elif len(username) > 20:
+			err = "username can't be longer than 20 characters"
+		elif not username.isalnum():
+			err = "username must be alphanumeric"
 
-        if err is None:
-            db.execute(
-                "INSERT INTO users (username, password, joined) VALUES (?,?,current_timestamp);",
-                (username,generate_password_hash(password))
-            )
-            db.commit()
-            session['user'] = username
-            if 'keep_logged_in' in request.form:
-                session['keep_logged_in'] = True
-                session.permanent = True
-            flash("successfully created account")
-            return redirect(get_next())
+		if err is None:
+			db.execute(
+				"INSERT INTO users (username, password, joined) VALUES (?,?,current_timestamp);",
+				(username,generate_password_hash(password))
+			)
+			db.commit()
+			session['user'] = username
+			if 'keep_logged_in' in request.form:
+				session['keep_logged_in'] = True
+				session.permanent = True
+			flash("successfully created account")
+			return redirect(get_next())
 
-        flash(err)
-            
-    return render_template("auth/register.html")
+		flash(err)
+			
+	return render_template("auth/register.html")
 
 @bp.route("/logout")
 def logout():
-    session.clear()
-    flash("logged out successfully")
-    return redirect(get_next())
+	session.clear()
+	flash("logged out successfully")
+	return redirect(get_next())
 
 @bp.before_app_request
 def load_user():
-    username = session.get("user")
-    if session.get("keep_logged_in",False):
-        session.permanent = True
-    if username is None:
-        g.user = None
-        g.user_info = None
-    else:
-        row = get_db().execute(
-            "SELECT * FROM users WHERE username = ?;", (username,)
-        ).fetchone()
-        if row is None:
-            g.user = None
-            g.user_info = None
-        else:
-            g.user = row['username']
-            g.user_info = row
-        
+	username = session.get("user")
+	if session.get("keep_logged_in",False):
+		session.permanent = True
+	if username is None:
+		g.user = None
+		g.user_info = None
+	else:
+		row = get_db().execute(
+			"SELECT * FROM users WHERE username = ?;", (username,)
+		).fetchone()
+		if row is None:
+			g.user = None
+			g.user_info = None
+		else:
+			g.user = row['username']
+			g.user_info = row
+		
 
 def login_required(view):
-    @functools.wraps(view)
-    def wrapped(**kwargs):
-        print(g.user)
-        if g.user is None:
-            return redirect(url_for("auth.login"))
-        return view(**kwargs)
-    return wrapped
+	@functools.wraps(view)
+	def wrapped(**kwargs):
+		print(g.user)
+		if g.user is None:
+			return redirect(url_for("auth.login"))
+		return view(**kwargs)
+	return wrapped
 
 @bp.route("/cool")
 def cool():
-    user = session.get("user")
-    if user is None:
-        return "you are not logged in"
-    else:
-        return f"you are logged in as {user}"
+	user = session.get("user")
+	if user is None:
+		return "you are not logged in"
+	else:
+		return f"you are logged in as {user}"
 
 @bp.route("/cooler")
 @login_required
 def cooler():
-    return "bee"
+	return "bee"
 
diff --git a/apioforum/csscolors.py b/apioforum/csscolors.py
index 25bdfdd..c8c096d 100644
--- a/apioforum/csscolors.py
+++ b/apioforum/csscolors.py
@@ -1,150 +1,150 @@
 csscolors = [
-    "black",
-    "silver",
-    "gray",
-    "white",
-    "maroon",
-    "red",
-    "purple",
-    "fuchsia",
-    "green",
-    "lime",
-    "olive",
-    "yellow",
-    "navy",
-    "blue",
-    "teal",
-    "aqua",
-    "orange",
-    "aliceblue",
-    "antiquewhite",
-    "aquamarine",
-    "azure",
-    "beige",
-    "bisque",
-    "blanchedalmond",
-    "blueviolet",
-    "brown",
-    "burlywood",
-    "cadetblue",
-    "chartreuse",
-    "chocolate",
-    "coral",
-    "cornflowerblue",
-    "cornsilk",
-    "crimson",
-    "cyan",
-    "darkblue",
-    "darkcyan",
-    "darkgoldenrod",
-    "darkgray",
-    "darkgreen",
-    "darkgrey",
-    "darkkhaki",
-    "darkmagenta",
-    "darkolivegreen",
-    "darkorange",
-    "darkorchid",
-    "darkred",
-    "darksalmon",
-    "darkseagreen",
-    "darkslateblue",
-    "darkslategray",
-    "darkslategrey",
-    "darkturquoise",
-    "darkviolet",
-    "deeppink",
-    "deepskyblue",
-    "dimgray",
-    "dimgrey",
-    "dodgerblue",
-    "firebrick",
-    "floralwhite",
-    "forestgreen",
-    "gainsboro",
-    "ghostwhite",
-    "gold",
-    "goldenrod",
-    "greenyellow",
-    "grey",
-    "honeydew",
-    "hotpink",
-    "indianred",
-    "indigo",
-    "ivory",
-    "khaki",
-    "lavender",
-    "lavenderblush",
-    "lawngreen",
-    "lemonchiffon",
-    "lightblue",
-    "lightcoral",
-    "lightcyan",
-    "lightgoldenrodyellow",
-    "lightgray",
-    "lightgreen",
-    "lightgrey",
-    "lightpink",
-    "lightsalmon",
-    "lightseagreen",
-    "lightskyblue",
-    "lightslategray",
-    "lightslategrey",
-    "lightsteelblue",
-    "lightyellow",
-    "limegreen",
-    "linen",
-    "magenta",
-    "mediumaquamarine",
-    "mediumblue",
-    "mediumorchid",
-    "mediumpurple",
-    "mediumseagreen",
-    "mediumslateblue",
-    "mediumspringgreen",
-    "mediumturquoise",
-    "mediumvioletred",
-    "midnightblue",
-    "mintcream",
-    "mistyrose",
-    "moccasin",
-    "navajowhite",
-    "oldlace",
-    "olivedrab",
-    "orangered",
-    "orchid",
-    "palegoldenrod",
-    "palegreen",
-    "paleturquoise",
-    "palevioletred",
-    "papayawhip",
-    "peachpuff",
-    "peru",
-    "pink",
-    "plum",
-    "powderblue",
-    "rosybrown",
-    "royalblue",
-    "saddlebrown",
-    "salmon",
-    "sandybrown",
-    "seagreen",
-    "seashell",
-    "sienna",
-    "skyblue",
-    "slateblue",
-    "slategray",
-    "slategrey",
-    "snow",
-    "springgreen",
-    "steelblue",
-    "tan",
-    "thistle",
-    "tomato",
-    "turquoise",
-    "violet",
-    "wheat",
-    "whitesmoke",
-    "yellowgreen",
-    "rebeccapurple"
+	"black",
+	"silver",
+	"gray",
+	"white",
+	"maroon",
+	"red",
+	"purple",
+	"fuchsia",
+	"green",
+	"lime",
+	"olive",
+	"yellow",
+	"navy",
+	"blue",
+	"teal",
+	"aqua",
+	"orange",
+	"aliceblue",
+	"antiquewhite",
+	"aquamarine",
+	"azure",
+	"beige",
+	"bisque",
+	"blanchedalmond",
+	"blueviolet",
+	"brown",
+	"burlywood",
+	"cadetblue",
+	"chartreuse",
+	"chocolate",
+	"coral",
+	"cornflowerblue",
+	"cornsilk",
+	"crimson",
+	"cyan",
+	"darkblue",
+	"darkcyan",
+	"darkgoldenrod",
+	"darkgray",
+	"darkgreen",
+	"darkgrey",
+	"darkkhaki",
+	"darkmagenta",
+	"darkolivegreen",
+	"darkorange",
+	"darkorchid",
+	"darkred",
+	"darksalmon",
+	"darkseagreen",
+	"darkslateblue",
+	"darkslategray",
+	"darkslategrey",
+	"darkturquoise",
+	"darkviolet",
+	"deeppink",
+	"deepskyblue",
+	"dimgray",
+	"dimgrey",
+	"dodgerblue",
+	"firebrick",
+	"floralwhite",
+	"forestgreen",
+	"gainsboro",
+	"ghostwhite",
+	"gold",
+	"goldenrod",
+	"greenyellow",
+	"grey",
+	"honeydew",
+	"hotpink",
+	"indianred",
+	"indigo",
+	"ivory",
+	"khaki",
+	"lavender",
+	"lavenderblush",
+	"lawngreen",
+	"lemonchiffon",
+	"lightblue",
+	"lightcoral",
+	"lightcyan",
+	"lightgoldenrodyellow",
+	"lightgray",
+	"lightgreen",
+	"lightgrey",
+	"lightpink",
+	"lightsalmon",
+	"lightseagreen",
+	"lightskyblue",
+	"lightslategray",
+	"lightslategrey",
+	"lightsteelblue",
+	"lightyellow",
+	"limegreen",
+	"linen",
+	"magenta",
+	"mediumaquamarine",
+	"mediumblue",
+	"mediumorchid",
+	"mediumpurple",
+	"mediumseagreen",
+	"mediumslateblue",
+	"mediumspringgreen",
+	"mediumturquoise",
+	"mediumvioletred",
+	"midnightblue",
+	"mintcream",
+	"mistyrose",
+	"moccasin",
+	"navajowhite",
+	"oldlace",
+	"olivedrab",
+	"orangered",
+	"orchid",
+	"palegoldenrod",
+	"palegreen",
+	"paleturquoise",
+	"palevioletred",
+	"papayawhip",
+	"peachpuff",
+	"peru",
+	"pink",
+	"plum",
+	"powderblue",
+	"rosybrown",
+	"royalblue",
+	"saddlebrown",
+	"salmon",
+	"sandybrown",
+	"seagreen",
+	"seashell",
+	"sienna",
+	"skyblue",
+	"slateblue",
+	"slategray",
+	"slategrey",
+	"snow",
+	"springgreen",
+	"steelblue",
+	"tan",
+	"thistle",
+	"tomato",
+	"turquoise",
+	"violet",
+	"wheat",
+	"whitesmoke",
+	"yellowgreen",
+	"rebeccapurple"
 ]
diff --git a/apioforum/db.py b/apioforum/db.py
index 2d06a13..620749f 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -4,40 +4,40 @@ from flask import current_app, g
 from flask.cli import with_appcontext
 
 def get_db():
-    if 'db' not in g:
-        g.db = sqlite3.connect(
-            current_app.config['DATABASE'],
-            detect_types=sqlite3.PARSE_DECLTYPES
-        )
-        g.db.row_factory = sqlite3.Row
-    g.db.execute("PRAGMA foreign_keys = ON;")
-    return g.db
+	if 'db' not in g:
+		g.db = sqlite3.connect(
+			current_app.config['DATABASE'],
+			detect_types=sqlite3.PARSE_DECLTYPES
+		)
+		g.db.row_factory = sqlite3.Row
+	g.db.execute("PRAGMA foreign_keys = ON;")
+	return g.db
 
 def close_db(e=None):
-    db = g.pop('db', None)
-    if db is not None:
-        db.close()
+	db = g.pop('db', None)
+	if db is not None:
+		db.close()
 
 migrations = [
 """
 CREATE TABLE users (
-    username TEXT PRIMARY KEY,
-    password TEXT NOT NULL
+	username TEXT PRIMARY KEY,
+	password TEXT NOT NULL
 );""",
 """
 CREATE TABLE threads (
-    id INTEGER PRIMARY KEY,
-    title TEXT NOT NULL,
-    creator TEXT NOT NULL REFERENCES users(username),
-    created TIMESTAMP NOT NULL,
-    updated TIMESTAMP NOT NULL
+	id INTEGER PRIMARY KEY,
+	title TEXT NOT NULL,
+	creator TEXT NOT NULL REFERENCES users(username),
+	created TIMESTAMP NOT NULL,
+	updated TIMESTAMP NOT NULL
 );
 CREATE TABLE posts (
-    id INTEGER PRIMARY KEY,
-    content TEXT,
-    thread INTEGER NOT NULL REFERENCES threads(id),
-    author TEXT NOT NULL REFERENCES users(username),
-    created TIMESTAMP NOT NULL
+	id INTEGER PRIMARY KEY,
+	content TEXT,
+	thread INTEGER NOT NULL REFERENCES threads(id),
+	author TEXT NOT NULL REFERENCES users(username),
+	created TIMESTAMP NOT NULL
 );
 
 CREATE INDEX posts_thread_idx ON posts (thread);
@@ -48,34 +48,34 @@ ALTER TABLE posts ADD COLUMN updated TIMESTAMP;
 """,
 """
 CREATE VIRTUAL TABLE posts_fts USING fts5(
-    content,
-    content=posts,
-    content_rowid=id,
-    tokenize='porter unicode61 remove_diacritics 2'
+	content,
+	content=posts,
+	content_rowid=id,
+	tokenize='porter unicode61 remove_diacritics 2'
 );
 INSERT INTO posts_fts (rowid, content) SELECT id, content FROM posts;
 
 CREATE TRIGGER posts_ai AFTER INSERT ON posts BEGIN
-    INSERT INTO posts_fts(rowid, content) VALUES (new.id, new.content);
+	INSERT INTO posts_fts(rowid, content) VALUES (new.id, new.content);
 END;
 CREATE TRIGGER posts_ad AFTER DELETE ON posts BEGIN
-    INSERT INTO posts_fts(posts_fts, rowid, content) VALUES('delete', old.id, old.content);
+	INSERT INTO posts_fts(posts_fts, rowid, content) VALUES('delete', old.id, old.content);
 END;
 CREATE TRIGGER posts_au AFTER UPDATE ON posts BEGIN
-    INSERT INTO posts_fts(posts_fts, rowid, content) VALUES('delete', old.id, old.content);
-    INSERT INTO posts_fts(rowid, content) VALUES (new.id, new.content);
+	INSERT INTO posts_fts(posts_fts, rowid, content) VALUES('delete', old.id, old.content);
+	INSERT INTO posts_fts(rowid, content) VALUES (new.id, new.content);
 END;
 """,
 """
 CREATE TABLE tags (
-    id INTEGER PRIMARY KEY,
-    name TEXT NOT NULL,
-    text_colour TEXT NOT NULL,
-    bg_colour TEXT NOT NULL
+	id INTEGER PRIMARY KEY,
+	name TEXT NOT NULL,
+	text_colour TEXT NOT NULL,
+	bg_colour TEXT NOT NULL
 );
 CREATE TABLE thread_tags (
-    thread INTEGER NOT NULL REFERENCES threads(id),
-    tag INTEGER NOT NULL REFERENCES tags(id)
+	thread INTEGER NOT NULL REFERENCES threads(id),
+	tag INTEGER NOT NULL REFERENCES tags(id)
 );
 """,
 """CREATE INDEX thread_tags_thread ON thread_tags (thread);""",
@@ -86,59 +86,59 @@ ALTER TABLE users ADD COLUMN joined TIMESTAMP;
 """,
 """
 CREATE TABLE polls (
-    id INTEGER PRIMARY KEY,
-    title TEXT NOT NULL
+	id INTEGER PRIMARY KEY,
+	title TEXT NOT NULL
 );
 ALTER TABLE threads ADD COLUMN poll INTEGER REFERENCES polls(id);
 
 CREATE TABLE poll_options (
-    poll INTEGER NOT NULL REFERENCES polls(id),
-    text TEXT NOT NULL,
-    option_idx INTEGER NOT NULL,
-    PRIMARY KEY ( poll, option_idx )
+	poll INTEGER NOT NULL REFERENCES polls(id),
+	text TEXT NOT NULL,
+	option_idx INTEGER NOT NULL,
+	PRIMARY KEY ( poll, option_idx )
 );
 
 CREATE TABLE votes (
-    id INTEGER PRIMARY KEY,
-    user TEXT NOT NULL REFERENCES users(username),
-    poll INTEGER NOT NULL,
-    option_idx INTEGER,
-    time TIMESTAMP NOT NULL,
-    current INTEGER NOT NULL,
-    is_retraction INTEGER,
-    CHECK (is_retraction OR (option_idx NOT NULL)),
-    FOREIGN KEY ( poll, option_idx ) REFERENCES poll_options(poll, option_idx)
+	id INTEGER PRIMARY KEY,
+	user TEXT NOT NULL REFERENCES users(username),
+	poll INTEGER NOT NULL,
+	option_idx INTEGER,
+	time TIMESTAMP NOT NULL,
+	current INTEGER NOT NULL,
+	is_retraction INTEGER,
+	CHECK (is_retraction OR (option_idx NOT NULL)),
+	FOREIGN KEY ( poll, option_idx ) REFERENCES poll_options(poll, option_idx)
 );
 ALTER TABLE posts ADD COLUMN vote INTEGER REFERENCES votes(id);
 """,
 """
 CREATE VIEW vote_counts AS
-    SELECT poll, option_idx, count(*) AS num FROM votes WHERE current GROUP BY option_idx,poll; 
+	SELECT poll, option_idx, count(*) AS num FROM votes WHERE current GROUP BY option_idx,poll; 
 """,
 """
 CREATE TABLE forums (
-    id INTEGER PRIMARY KEY,
-    name TEXT NOT NULL,
-    parent INTEGER REFERENCES forums(id),
-    description TEXT
+	id INTEGER PRIMARY KEY,
+	name TEXT NOT NULL,
+	parent INTEGER REFERENCES forums(id),
+	description TEXT
 );
 INSERT INTO forums (name,parent,description) values ('apioforum',null,
-        'welcome to the apioforum\n\n' ||
-        'forum rules: do not be a bad person. do not do bad things.');
+		'welcome to the apioforum\n\n' ||
+		'forum rules: do not be a bad person. do not do bad things.');
 
 PRAGMA foreign_keys = off;
 BEGIN TRANSACTION;
 CREATE TABLE threads_new (
-    id INTEGER PRIMARY KEY,
-    title TEXT NOT NULL,
-    creator TEXT NOT NULL,
-    created TIMESTAMP NOT NULL,
-    updated TIMESTAMP NOT NULL,
-    forum NOT NULL REFERENCES forums(id),
-    poll INTEGER REFERENCES polls(id)
+	id INTEGER PRIMARY KEY,
+	title TEXT NOT NULL,
+	creator TEXT NOT NULL,
+	created TIMESTAMP NOT NULL,
+	updated TIMESTAMP NOT NULL,
+	forum NOT NULL REFERENCES forums(id),
+	poll INTEGER REFERENCES polls(id)
 );
 INSERT INTO threads_new (id,title,creator,created,updated,forum)
-    SELECT id,title,creator,created,updated,1 FROM threads;
+	SELECT id,title,creator,created,updated,1 FROM threads;
 DROP TABLE threads;
 ALTER TABLE threads_new RENAME TO threads;
 COMMIT;
@@ -146,14 +146,14 @@ PRAGMA foreign_keys = on;
 """,
 """
 CREATE VIEW most_recent_posts AS
-    SELECT max(id), * FROM posts GROUP BY thread;
+	SELECT max(id), * FROM posts GROUP BY thread;
 
 CREATE VIEW number_of_posts AS
-    SELECT thread, count(*) AS num_replies FROM posts GROUP BY thread;
+	SELECT thread, count(*) AS num_replies FROM posts GROUP BY thread;
 """,
 """
 CREATE VIEW total_vote_counts AS
-    SELECT poll, count(*) AS total_votes FROM votes WHERE current AND NOT is_retraction GROUP BY poll;
+	SELECT poll, count(*) AS total_votes FROM votes WHERE current AND NOT is_retraction GROUP BY poll;
 """,
 """
 PRAGMA foreign_keys = off;
@@ -173,19 +173,19 @@ PRAGMA foreign_keys = on;
 """,
 """
 CREATE TABLE role_config (
-    role TEXT NOT NULL,
-    forum NOT NULL REFERENCES forums(id),
-    id INTEGER PRIMARY KEY,
+	role TEXT NOT NULL,
+	forum NOT NULL REFERENCES forums(id),
+	id INTEGER PRIMARY KEY,
 
-    p_create_threads INT NOT NULL DEFAULT 1,
-    p_reply_threads INT NOT NULL DEFAULT 1,
-    p_view_threads INT NOT NULL DEFAULT 1,
-    p_manage_threads INT NOT NULL DEFAULT 0,
-    p_delete_posts INT NOT NULL DEFAULT 0,
-    p_vote INT NOT NULL DEFAULT 1,
-    p_create_polls INT NOT NULL DEFAULT 1,
-    p_approve INT NOT NULL DEFAULT 0,
-    p_create_subforum INT NOT NULL DEFAULT 0
+	p_create_threads INT NOT NULL DEFAULT 1,
+	p_reply_threads INT NOT NULL DEFAULT 1,
+	p_view_threads INT NOT NULL DEFAULT 1,
+	p_manage_threads INT NOT NULL DEFAULT 0,
+	p_delete_posts INT NOT NULL DEFAULT 0,
+	p_vote INT NOT NULL DEFAULT 1,
+	p_create_polls INT NOT NULL DEFAULT 1,
+	p_approve INT NOT NULL DEFAULT 0,
+	p_create_subforum INT NOT NULL DEFAULT 0
 );
 
 INSERT INTO role_config (role,forum) VALUES ("approved",1);
@@ -193,9 +193,9 @@ INSERT INTO role_config (role,forum) VALUES ("other",1);
 """,
 """
 CREATE TABLE role_assignments (
-    user NOT NULL REFERENCES users(username),
-    forum NOT NULL REFERENCES forums(id),
-    role TEXT NOT NULL
+	user NOT NULL REFERENCES users(username),
+	forum NOT NULL REFERENCES forums(id),
+	role TEXT NOT NULL
 );
 """,
 """
@@ -209,36 +209,36 @@ ALTER TABLE role_config ADD COLUMN p_view_forum INT NOT NULL DEFAULT 1;
 """,
 """
 CREATE TABLE webhooks (
-    id INTEGER PRIMARY KEY,
-    type TEXT NOT NULL,
-    url TEXT NOT NULL,
-    forum INTEGER NOT NULL REFERENCES forums(id)
+	id INTEGER PRIMARY KEY,
+	type TEXT NOT NULL,
+	url TEXT NOT NULL,
+	forum INTEGER NOT NULL REFERENCES forums(id)
 );""",
 """
 CREATE VIEW public_forums AS
-    SELECT f.id as id,
-        COALESCE(r.p_view_forum,1) as public
-    FROM forums f
-    LEFT JOIN role_config r ON
-    r.forum = f.id AND r.role='other';
+	SELECT f.id as id,
+		COALESCE(r.p_view_forum,1) as public
+	FROM forums f
+	LEFT JOIN role_config r ON
+	r.forum = f.id AND r.role='other';
 CREATE VIEW forum_thread_of_post AS
-    SELECT p.id as p_id, t.id as t_id, f.id as f_id
-    FROM posts p
-    JOIN threads t on p.thread = t.id
-    JOIN forums f on t.forum = f.id;
+	SELECT p.id as p_id, t.id as t_id, f.id as f_id
+	FROM posts p
+	JOIN threads t on p.thread = t.id
+	JOIN forums f on t.forum = f.id;
 CREATE VIEW public_posts AS
-    SELECT p.id AS id,
-      b.public AS public
-    FROM posts p
-    JOIN forum_thread_of_post h ON p.id=h.p_id
-    JOIN public_forums b ON b.id=h.f_id;
+	SELECT p.id AS id,
+	  b.public AS public
+	FROM posts p
+	JOIN forum_thread_of_post h ON p.id=h.p_id
+	JOIN public_forums b ON b.id=h.f_id;
 """,
 """
 CREATE TABLE read (
-    user NOT NULL REFERENCES users(username),
-    forum REFERENCES forums(id),
-    thread REFERENCES threads(id),
-    time TIMESTAMP NOT NULL
+	user NOT NULL REFERENCES users(username),
+	forum REFERENCES forums(id),
+	thread REFERENCES threads(id),
+	time TIMESTAMP NOT NULL
 );
 ALTER TABLE forums ADD COLUMN updated TIMESTAMP;
 """,
@@ -246,22 +246,22 @@ ALTER TABLE forums ADD COLUMN updated TIMESTAMP;
 
 
 def init_db():
-    db = get_db()
-    version = db.execute("PRAGMA user_version;").fetchone()[0]
-    for i in range(version, len(migrations)):
-        db.executescript(migrations[i])
-        db.execute(f"PRAGMA user_version = {i+1}")
-        db.commit()
-        click.echo(f"migration {i}")
+	db = get_db()
+	version = db.execute("PRAGMA user_version;").fetchone()[0]
+	for i in range(version, len(migrations)):
+		db.executescript(migrations[i])
+		db.execute(f"PRAGMA user_version = {i+1}")
+		db.commit()
+		click.echo(f"migration {i}")
 
 @click.command("migrate")
 @with_appcontext
 def migrate_command():
-    """update database scheme etc"""
-    init_db()
-    click.echo("ok")
+	"""update database scheme etc"""
+	init_db()
+	click.echo("ok")
 
 def init_app(app):
-    app.teardown_appcontext(close_db)
-    app.cli.add_command(migrate_command)
+	app.teardown_appcontext(close_db)
+	app.cli.add_command(migrate_command)
 
diff --git a/apioforum/forum.py b/apioforum/forum.py
index b86fcc9..ce90853 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -3,8 +3,8 @@
 # ^ aha we never removed this. we should keep it. it is funny.
 
 from flask import (
-    Blueprint, render_template, request,
-    g, redirect, url_for, flash, abort
+	Blueprint, render_template, request,
+	g, redirect, url_for, flash, abort
 )
 
 from .db import get_db
@@ -23,484 +23,484 @@ bp = Blueprint("forum", __name__, url_prefix="/")
 
 @bp.route("/")
 def not_actual_index():
-    return redirect("/1")
+	return redirect("/1")
 
 def get_avail_tags(forum_id):
-    db = get_db()
-    tags = db.execute("""
-    	WITH RECURSIVE fs AS
-    		(SELECT * FROM forums WHERE id = ?
-    		 UNION ALL
-    		 SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
-    	SELECT * FROM tags
-    	WHERE tags.forum in (SELECT id FROM fs)
-    	ORDER BY id;    	
-    	""",(forum_id,)).fetchall()
-    return tags 
+	db = get_db()
+	tags = db.execute("""
+		WITH RECURSIVE fs AS
+			(SELECT * FROM forums WHERE id = ?
+			 UNION ALL
+			 SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
+		SELECT * FROM tags
+		WHERE tags.forum in (SELECT id FROM fs)
+		ORDER BY id;		
+		""",(forum_id,)).fetchall()
+	return tags 
 
 def forum_path(forum_id):
-    db = get_db()
-    ancestors = db.execute("""
-        WITH RECURSIVE fs AS
-            (SELECT * FROM forums WHERE id = ?
-             UNION ALL
-             SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
-        SELECT * FROM fs;
-        """,(forum_id,)).fetchall()
-    ancestors.reverse()
-    return ancestors
+	db = get_db()
+	ancestors = db.execute("""
+		WITH RECURSIVE fs AS
+			(SELECT * FROM forums WHERE id = ?
+			 UNION ALL
+			 SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
+		SELECT * FROM fs;
+		""",(forum_id,)).fetchall()
+	ancestors.reverse()
+	return ancestors
 
 def forum_route(relative_path, pagination=False, **kwargs):
-    def decorator(f):
-        path = "/<int:forum_id>"
-        if relative_path != "":
-            path += "/" + relative_path
-
-        @bp.route(path, **kwargs)
-        @functools.wraps(f)
-        def wrapper(forum_id, *args, **kwargs):
-            db = get_db()
-            forum = db.execute("SELECT * FROM forums WHERE id = ?",
-                    (forum_id,)).fetchone()
-            if forum == None:
-                abort(404)
-            return f(forum, *args, **kwargs)
-
-        if pagination:
-            wrapper = bp.route(path+"/page/<int:page>", **kwargs)(wrapper)
-
-    return decorator
+	def decorator(f):
+		path = "/<int:forum_id>"
+		if relative_path != "":
+			path += "/" + relative_path
+
+		@bp.route(path, **kwargs)
+		@functools.wraps(f)
+		def wrapper(forum_id, *args, **kwargs):
+			db = get_db()
+			forum = db.execute("SELECT * FROM forums WHERE id = ?",
+					(forum_id,)).fetchone()
+			if forum == None:
+				abort(404)
+			return f(forum, *args, **kwargs)
+
+		if pagination:
+			wrapper = bp.route(path+"/page/<int:page>", **kwargs)(wrapper)
+
+	return decorator
 
 def requires_permission(permission, login_required=True):
-    def decorator(f):
-        @functools.wraps(f)
-        def wrapper(forum, *args, **kwargs):
-            if not has_permission(forum['id'],g.user,permission,login_required):
-                abort(403)
-            return f(forum, *args, **kwargs)
-        return wrapper
-    return decorator
+	def decorator(f):
+		@functools.wraps(f)
+		def wrapper(forum, *args, **kwargs):
+			if not has_permission(forum['id'],g.user,permission,login_required):
+				abort(403)
+			return f(forum, *args, **kwargs)
+		return wrapper
+	return decorator
 
 def requires_bureaucrat(f):
-    @functools.wraps(f)
-    @requires_permission("p_view_forum")
-    def wrapper(forum, *args, **kwargs):
-        if not is_bureaucrat(forum['id'], g.user):
-            abort(403)
-        return f(forum, *args, **kwargs)
-    return wrapper
+	@functools.wraps(f)
+	@requires_permission("p_view_forum")
+	def wrapper(forum, *args, **kwargs):
+		if not is_bureaucrat(forum['id'], g.user):
+			abort(403)
+		return f(forum, *args, **kwargs)
+	return wrapper
 
 def set_updated(forum_id):
-    db = get_db()
-    ancestors = db.execute("""
-        WITH RECURSIVE fs AS
-            (SELECT * FROM forums WHERE id = ?
-             UNION ALL
-             SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
-        SELECT * FROM fs;
-        """,(forum_id,)).fetchall()
-    for f in ancestors:
-        db.execute("UPDATE forums SET updated = current_timestamp WHERE id = ?;", (f['id'],))
-    db.commit()
+	db = get_db()
+	ancestors = db.execute("""
+		WITH RECURSIVE fs AS
+			(SELECT * FROM forums WHERE id = ?
+			 UNION ALL
+			 SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
+		SELECT * FROM fs;
+		""",(forum_id,)).fetchall()
+	for f in ancestors:
+		db.execute("UPDATE forums SET updated = current_timestamp WHERE id = ?;", (f['id'],))
+	db.commit()
 
 @forum_route("",pagination=True)
 @requires_permission("p_view_forum", login_required=False)
 def view_forum(forum,page=1):
-    db = get_db()
-
-    sortby = request.args.get("sortby","ad")
-    try:
-        sortby_dir = {'d':'DESC','a':'ASC'}[sortby[1]]
-        sortby_by  = {'a':'threads.updated','c':'threads.created'}[sortby[0]]
-    except KeyError:
-        return redirect(url_for('forum.view_forum',forum_id=forum['id']))
-
-    avail_tags = get_avail_tags(forum['id'])
-
-    tagfilter = request.args.get("tagfilter",None)
-    if tagfilter == "":
-        tagfilter = None
-    tagfilter_clause = ""
-    tagfilter_tag = None
-    if tagfilter is not None:
-        try:
-            tagfilter = int(tagfilter)
-        except ValueError:
-            flash(f'invalid tag id "{tagfilter}"')
-            return redirect(url_for('forum.view_forum',forum_id=forum['id']))
-        else:
-            # there is no risk of sql injection because
-            # we just checked it is an int
-            tagfilter_clause = f"AND thread_tags.tag = {tagfilter}"
-            for the_tag in avail_tags:
-                if the_tag['id'] == tagfilter:
-                    tagfilter_tag = the_tag
-                    break
-            else:
-                flash("that tag doesn't exist or isn't available here")
-                return redirect(url_for('forum.view_forum',forum_id=forum['id']))
-               
-
-    threads = db.execute(
-        f"""SELECT
-            threads.id, threads.title, threads.creator, threads.created,
-            threads.updated, threads.poll, number_of_posts.num_replies,
-            most_recent_posts.created as mrp_created,
-            most_recent_posts.author as mrp_author,
-            most_recent_posts.id as mrp_id,
-            most_recent_posts.content as mrp_content,
-            most_recent_posts.deleted as mrp_deleted
-        FROM threads
-        INNER JOIN most_recent_posts ON most_recent_posts.thread = threads.id
-        INNER JOIN number_of_posts ON number_of_posts.thread = threads.id
-        LEFT OUTER JOIN thread_tags ON threads.id = thread_tags.thread
-        WHERE threads.forum = ? {tagfilter_clause}
-        GROUP BY threads.id
-        ORDER BY {sortby_by} {sortby_dir}
-        LIMIT ? OFFSET ?;
-        """,(
-            forum['id'],
-            THREADS_PER_PAGE,
-            (page-1)*THREADS_PER_PAGE,
-        )).fetchall()
-
-    num_threads = db.execute(f"""
-        SELECT count(*) AS count FROM threads
-        LEFT OUTER JOIN thread_tags ON threads.id = thread_tags.thread
-        WHERE threads.forum = ? {tagfilter_clause};
-    """,(forum['id'],)).fetchone()['count']
-    max_pageno = math.ceil(num_threads/THREADS_PER_PAGE)
-    if page < 1:
-        abort(404)
-    elif page > max_pageno and (max_pageno > 0 or page != 1):
-        abort(404)
-    
-    thread_tags = {}
-    thread_polls = {}
-
-
-    #todo: somehow optimise this
-    for thread in threads:
-        thread_tags[thread['id']] = db.execute(
-            """SELECT tags.* FROM tags
-            INNER JOIN thread_tags ON thread_tags.tag = tags.id
-            WHERE thread_tags.thread = ?
-            ORDER BY tags.id;
-            """,(thread['id'],)).fetchall()
-
-        if thread['poll'] is not None:
-            # todo: make this not be duplicated from thread.py
-            poll_row= db.execute("""
-                SELECT polls.*,total_vote_counts.total_votes FROM polls
-                LEFT OUTER JOIN total_vote_counts ON polls.id = total_vote_counts.poll
-                WHERE polls.id = ?;                
-                """,(thread['poll'],)).fetchone()
-            options = db.execute("""
-                SELECT poll_options.*, vote_counts.num
-                FROM poll_options
-                LEFT OUTER JOIN vote_counts  ON poll_options.poll = vote_counts.poll
-                                            AND poll_options.option_idx = vote_counts.option_idx
-                WHERE poll_options.poll = ?
-                ORDER BY option_idx asc;
-                """,(poll_row['id'],)).fetchall()
-
-            poll = {}
-            poll.update(poll_row)
-            poll['options'] = options
-            poll['total_votes']=poll['total_votes'] or 0
-            thread_polls[thread['id']]=poll
-
-
-    subforums_rows = db.execute("""
-            SELECT max(threads.updated) as updated, forums.* FROM forums
-            LEFT OUTER JOIN threads ON threads.forum=forums.id 
-            WHERE parent = ? AND unlisted = 0
-            GROUP BY forums.id
-            ORDER BY name ASC
-            """,(forum['id'],)).fetchall()
-    subforums = []
-    for s in subforums_rows:
-        a={}
-        a.update(s)
-        if a['updated'] is not None:
-            a['updated'] = datetime.datetime.fromisoformat(a['updated'])
-        if has_permission(a['id'],g.user,"p_view_forum",login_required=False):
-            subforums.append(a)
-
-    bureaucrats = db.execute("""
-            SELECT user FROM role_assignments
-            WHERE role = 'bureaucrat' AND forum = ?
-            """,(forum['id'],)).fetchall()
-    bureaucrats = [b[0] for b in bureaucrats]
-
-
-    if g.user != None:
-        db.execute("DELETE FROM read WHERE user = ? AND forum = ?;", (g.user, forum['id']))
-        db.execute("INSERT INTO read (user,forum,time) VALUES (?,?,current_timestamp);", (g.user, forum['id']))
-        db.commit()
-
-        
-    return render_template("view_forum.html",
-            forum=forum,
-            subforums=subforums,
-            threads=threads,
-            thread_tags=thread_tags,
-            bureaucrats=bureaucrats,
-            thread_polls=thread_polls,
-            avail_tags=avail_tags,
-            max_pageno=max_pageno,
-            page=page,
-            current_sortby=sortby,
-            tagfilter_tag=tagfilter_tag,
-            is_read=read.is_read,
-            )
+	db = get_db()
+
+	sortby = request.args.get("sortby","ad")
+	try:
+		sortby_dir = {'d':'DESC','a':'ASC'}[sortby[1]]
+		sortby_by  = {'a':'threads.updated','c':'threads.created'}[sortby[0]]
+	except KeyError:
+		return redirect(url_for('forum.view_forum',forum_id=forum['id']))
+
+	avail_tags = get_avail_tags(forum['id'])
+
+	tagfilter = request.args.get("tagfilter",None)
+	if tagfilter == "":
+		tagfilter = None
+	tagfilter_clause = ""
+	tagfilter_tag = None
+	if tagfilter is not None:
+		try:
+			tagfilter = int(tagfilter)
+		except ValueError:
+			flash(f'invalid tag id "{tagfilter}"')
+			return redirect(url_for('forum.view_forum',forum_id=forum['id']))
+		else:
+			# there is no risk of sql injection because
+			# we just checked it is an int
+			tagfilter_clause = f"AND thread_tags.tag = {tagfilter}"
+			for the_tag in avail_tags:
+				if the_tag['id'] == tagfilter:
+					tagfilter_tag = the_tag
+					break
+			else:
+				flash("that tag doesn't exist or isn't available here")
+				return redirect(url_for('forum.view_forum',forum_id=forum['id']))
+			   
+
+	threads = db.execute(
+		f"""SELECT
+			threads.id, threads.title, threads.creator, threads.created,
+			threads.updated, threads.poll, number_of_posts.num_replies,
+			most_recent_posts.created as mrp_created,
+			most_recent_posts.author as mrp_author,
+			most_recent_posts.id as mrp_id,
+			most_recent_posts.content as mrp_content,
+			most_recent_posts.deleted as mrp_deleted
+		FROM threads
+		INNER JOIN most_recent_posts ON most_recent_posts.thread = threads.id
+		INNER JOIN number_of_posts ON number_of_posts.thread = threads.id
+		LEFT OUTER JOIN thread_tags ON threads.id = thread_tags.thread
+		WHERE threads.forum = ? {tagfilter_clause}
+		GROUP BY threads.id
+		ORDER BY {sortby_by} {sortby_dir}
+		LIMIT ? OFFSET ?;
+		""",(
+			forum['id'],
+			THREADS_PER_PAGE,
+			(page-1)*THREADS_PER_PAGE,
+		)).fetchall()
+
+	num_threads = db.execute(f"""
+		SELECT count(*) AS count FROM threads
+		LEFT OUTER JOIN thread_tags ON threads.id = thread_tags.thread
+		WHERE threads.forum = ? {tagfilter_clause};
+	""",(forum['id'],)).fetchone()['count']
+	max_pageno = math.ceil(num_threads/THREADS_PER_PAGE)
+	if page < 1:
+		abort(404)
+	elif page > max_pageno and (max_pageno > 0 or page != 1):
+		abort(404)
+	
+	thread_tags = {}
+	thread_polls = {}
+
+
+	#todo: somehow optimise this
+	for thread in threads:
+		thread_tags[thread['id']] = db.execute(
+			"""SELECT tags.* FROM tags
+			INNER JOIN thread_tags ON thread_tags.tag = tags.id
+			WHERE thread_tags.thread = ?
+			ORDER BY tags.id;
+			""",(thread['id'],)).fetchall()
+
+		if thread['poll'] is not None:
+			# todo: make this not be duplicated from thread.py
+			poll_row= db.execute("""
+				SELECT polls.*,total_vote_counts.total_votes FROM polls
+				LEFT OUTER JOIN total_vote_counts ON polls.id = total_vote_counts.poll
+				WHERE polls.id = ?;				
+				""",(thread['poll'],)).fetchone()
+			options = db.execute("""
+				SELECT poll_options.*, vote_counts.num
+				FROM poll_options
+				LEFT OUTER JOIN vote_counts  ON poll_options.poll = vote_counts.poll
+											AND poll_options.option_idx = vote_counts.option_idx
+				WHERE poll_options.poll = ?
+				ORDER BY option_idx asc;
+				""",(poll_row['id'],)).fetchall()
+
+			poll = {}
+			poll.update(poll_row)
+			poll['options'] = options
+			poll['total_votes']=poll['total_votes'] or 0
+			thread_polls[thread['id']]=poll
+
+
+	subforums_rows = db.execute("""
+			SELECT max(threads.updated) as updated, forums.* FROM forums
+			LEFT OUTER JOIN threads ON threads.forum=forums.id 
+			WHERE parent = ? AND unlisted = 0
+			GROUP BY forums.id
+			ORDER BY name ASC
+			""",(forum['id'],)).fetchall()
+	subforums = []
+	for s in subforums_rows:
+		a={}
+		a.update(s)
+		if a['updated'] is not None:
+			a['updated'] = datetime.datetime.fromisoformat(a['updated'])
+		if has_permission(a['id'],g.user,"p_view_forum",login_required=False):
+			subforums.append(a)
+
+	bureaucrats = db.execute("""
+			SELECT user FROM role_assignments
+			WHERE role = 'bureaucrat' AND forum = ?
+			""",(forum['id'],)).fetchall()
+	bureaucrats = [b[0] for b in bureaucrats]
+
+
+	if g.user != None:
+		db.execute("DELETE FROM read WHERE user = ? AND forum = ?;", (g.user, forum['id']))
+		db.execute("INSERT INTO read (user,forum,time) VALUES (?,?,current_timestamp);", (g.user, forum['id']))
+		db.commit()
+
+		
+	return render_template("view_forum.html",
+			forum=forum,
+			subforums=subforums,
+			threads=threads,
+			thread_tags=thread_tags,
+			bureaucrats=bureaucrats,
+			thread_polls=thread_polls,
+			avail_tags=avail_tags,
+			max_pageno=max_pageno,
+			page=page,
+			current_sortby=sortby,
+			tagfilter_tag=tagfilter_tag,
+			is_read=read.is_read,
+			)
 
 @forum_route("create_thread",methods=("GET","POST"))
 @requires_permission("p_create_threads")
 @requires_permission("p_view_forum")
 def create_thread(forum):
-    db = get_db()
-    forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum['id'],)).fetchone()
-    if forum is None:
-        flash("that forum doesn't exist")
-        return redirect(url_for('index'))
-    
-    if g.user is None:
-        flash("you need to be logged in to create a thread")
-        return redirect(url_for('index'))
-        
-    if request.method == "POST":
-        title = request.form['title']
-        content = request.form['content']
-        err = None
-        if len(title.strip()) == 0 or len(content.strip()) == 0:
-            err = "title and content can't be empty"
-
-        if err is None:
-            cur = db.cursor()
-            cur.execute(
-                "INSERT INTO threads (title,creator,created,updated,forum) VALUES (?,?,current_timestamp,current_timestamp,?);",
-                (title,g.user,forum['id'])
-            )
-            thread_id = cur.lastrowid
-            cur.execute(
-                "INSERT INTO posts (thread,created,author,content) VALUES (?,current_timestamp,?,?);",
-                (thread_id,g.user,content)
-            )
-            db.commit()
-            set_updated(forum['id'])
-
-            from . import webhooks
-            thread = db.execute("select * from threads where id = ?",(thread_id,)).fetchone()
-            webhooks.do_webhooks_thread(forum['id'],thread)
-            return redirect(url_for('thread.view_thread',thread_id=thread_id))
-        flash(err)
-        
-        
-    return render_template("create_thread.html")
+	db = get_db()
+	forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum['id'],)).fetchone()
+	if forum is None:
+		flash("that forum doesn't exist")
+		return redirect(url_for('index'))
+	
+	if g.user is None:
+		flash("you need to be logged in to create a thread")
+		return redirect(url_for('index'))
+		
+	if request.method == "POST":
+		title = request.form['title']
+		content = request.form['content']
+		err = None
+		if len(title.strip()) == 0 or len(content.strip()) == 0:
+			err = "title and content can't be empty"
+
+		if err is None:
+			cur = db.cursor()
+			cur.execute(
+				"INSERT INTO threads (title,creator,created,updated,forum) VALUES (?,?,current_timestamp,current_timestamp,?);",
+				(title,g.user,forum['id'])
+			)
+			thread_id = cur.lastrowid
+			cur.execute(
+				"INSERT INTO posts (thread,created,author,content) VALUES (?,current_timestamp,?,?);",
+				(thread_id,g.user,content)
+			)
+			db.commit()
+			set_updated(forum['id'])
+
+			from . import webhooks
+			thread = db.execute("select * from threads where id = ?",(thread_id,)).fetchone()
+			webhooks.do_webhooks_thread(forum['id'],thread)
+			return redirect(url_for('thread.view_thread',thread_id=thread_id))
+		flash(err)
+		
+		
+	return render_template("create_thread.html")
 
 @forum_route("roles",methods=("GET","POST"))
 @requires_bureaucrat
 def edit_roles(forum):
-    db = get_db()
-    role_configs = db.execute(
-        "SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC",
-        (forum['id'],)).fetchall()
-
-    if request.method == "POST":
-        for config in role_configs:
-            if 'delete_' + config['role'] in request.form:
-                db.execute(
-                    "DELETE FROM role_config WHERE forum = ? AND role = ?",
-                    (forum['id'],config['role']))
-            elif 'roleconfig_' + config['role'] in request.form:
-                for p in role_permissions:
-                    permission_setting =\
-                        f"perm_{config['role']}_{p}" in request.form 
-                    db.execute(f"""
-                        UPDATE role_config SET {p} = ?
-                            WHERE forum = ? AND role = ?;
-                        """, 
-                        (permission_setting,forum['id'], config['role']))
-        db.commit()
-        flash('roles sucessfully enroled')
-        return redirect(url_for('forum.view_forum',forum_id=forum['id']))
-
-    role_config_roles = [c['role'] for c in role_configs]
-    other_roles = [role for role in get_forum_roles(forum['id']) if not role in role_config_roles]
-
-    return render_template("edit_permissions.html",
-            forum=forum,
-            role_configs=role_configs,
-            other_roles=other_roles
-            )
+	db = get_db()
+	role_configs = db.execute(
+		"SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC",
+		(forum['id'],)).fetchall()
+
+	if request.method == "POST":
+		for config in role_configs:
+			if 'delete_' + config['role'] in request.form:
+				db.execute(
+					"DELETE FROM role_config WHERE forum = ? AND role = ?",
+					(forum['id'],config['role']))
+			elif 'roleconfig_' + config['role'] in request.form:
+				for p in role_permissions:
+					permission_setting =\
+						f"perm_{config['role']}_{p}" in request.form 
+					db.execute(f"""
+						UPDATE role_config SET {p} = ?
+							WHERE forum = ? AND role = ?;
+						""", 
+						(permission_setting,forum['id'], config['role']))
+		db.commit()
+		flash('roles sucessfully enroled')
+		return redirect(url_for('forum.view_forum',forum_id=forum['id']))
+
+	role_config_roles = [c['role'] for c in role_configs]
+	other_roles = [role for role in get_forum_roles(forum['id']) if not role in role_config_roles]
+
+	return render_template("edit_permissions.html",
+			forum=forum,
+			role_configs=role_configs,
+			other_roles=other_roles
+			)
 
 @forum_route("roles/new",methods=["POST"])
 @requires_bureaucrat
 def add_role(forum):
-    name = request.form['role'].strip()
-    if not all(c in (" ","-","_") or c.isalnum() for c in name) \
-            or len(name) > 32:
-        flash("role name must contain no special characters")
-        return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
-    if name == "bureaucrat":
-        flash("cannot configure permissions for bureaucrat")
-        return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
-
-    db = get_db()
-
-    existing_config = db.execute("""
-        SELECT * FROM role_config WHERE forum = ? AND role = ?
-        """,(forum['id'],name)).fetchone()
-    if not existing_config:
-        db.execute("INSERT INTO role_config (forum,role) VALUES (?,?)",
-                (forum['id'],name))
-        db.commit()
-    return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
+	name = request.form['role'].strip()
+	if not all(c in (" ","-","_") or c.isalnum() for c in name) \
+			or len(name) > 32:
+		flash("role name must contain no special characters")
+		return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
+	if name == "bureaucrat":
+		flash("cannot configure permissions for bureaucrat")
+		return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
+
+	db = get_db()
+
+	existing_config = db.execute("""
+		SELECT * FROM role_config WHERE forum = ? AND role = ?
+		""",(forum['id'],name)).fetchone()
+	if not existing_config:
+		db.execute("INSERT INTO role_config (forum,role) VALUES (?,?)",
+				(forum['id'],name))
+		db.commit()
+	return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
 
 @forum_route("role",methods=["GET","POST"])
 @requires_permission("p_approve")
 def view_user_role(forum):
-    if request.method == "POST":
-        return redirect(url_for( 'forum.edit_user_role',
-            username=request.form['user'],forum_id=forum['id']))
-    else:
-        return render_template("role_assignment.html",forum=forum)
+	if request.method == "POST":
+		return redirect(url_for( 'forum.edit_user_role',
+			username=request.form['user'],forum_id=forum['id']))
+	else:
+		return render_template("role_assignment.html",forum=forum)
 
 @forum_route("role/<username>",methods=["GET","POST"])
 @requires_permission("p_approve")
 def edit_user_role(forum, username):
-    db = get_db()
-    if request.method == "POST":
-        user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
-        if user == None:
-            return redirect(url_for('forum.edit_user_role',
-                username=username,forum_id=forum['id']))
-        role = request.form['role']
-        if role not in get_forum_roles(forum['id']) and role != "" and role != "bureaucrat":
-            flash("no such role")
-            return redirect(url_for('forum.edit_user_role',
-                username=username,forum_id=forum['id']))
-        if not is_bureaucrat(forum['id'],g.user) and role != "approved" and role != "":
-            # only bureaucrats can assign arbitrary roles
-            abort(403)
-        existing = db.execute(
-            "SELECT * FROM role_assignments WHERE user = ? AND forum = ?;",
-                (username,forum['id'])).fetchone()
-        if existing:
-            db.execute("DELETE FROM role_assignments WHERE user = ? AND forum = ?;",(username,forum['id']))
-        if role != "":
-            db.execute(
-                "INSERT INTO role_assignments (user,role,forum) VALUES (?,?,?);",
-                (username,role,forum['id']))
-        db.commit()
-        flash("role assigned assignedly")
-        return redirect(url_for('forum.view_forum',forum_id=forum['id']))
-    else:
-        user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
-        if user == None:
-            return render_template("role_assignment.html",
-                    forum=forum,user=username,invalid_user=True)
-        r = db.execute(
-                "SELECT role FROM role_assignments WHERE user = ? AND forum = ?;",
-                    (username,forum['id'])).fetchone()
-        if not r:
-            assigned_role = ""
-        else:
-            assigned_role = r[0]
-        role = get_user_role(forum['id'], username)
-        if is_bureaucrat(forum['id'], g.user):
-            roles = get_forum_roles(forum['id'])
-            roles.remove("other")
-            roles.add("bureaucrat")
-        else:
-            roles = ["approved"]
-        return render_template("role_assignment.html",
-                forum=forum,user=username,role=role,
-                assigned_role=assigned_role,forum_roles=roles)
+	db = get_db()
+	if request.method == "POST":
+		user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
+		if user == None:
+			return redirect(url_for('forum.edit_user_role',
+				username=username,forum_id=forum['id']))
+		role = request.form['role']
+		if role not in get_forum_roles(forum['id']) and role != "" and role != "bureaucrat":
+			flash("no such role")
+			return redirect(url_for('forum.edit_user_role',
+				username=username,forum_id=forum['id']))
+		if not is_bureaucrat(forum['id'],g.user) and role != "approved" and role != "":
+			# only bureaucrats can assign arbitrary roles
+			abort(403)
+		existing = db.execute(
+			"SELECT * FROM role_assignments WHERE user = ? AND forum = ?;",
+				(username,forum['id'])).fetchone()
+		if existing:
+			db.execute("DELETE FROM role_assignments WHERE user = ? AND forum = ?;",(username,forum['id']))
+		if role != "":
+			db.execute(
+				"INSERT INTO role_assignments (user,role,forum) VALUES (?,?,?);",
+				(username,role,forum['id']))
+		db.commit()
+		flash("role assigned assignedly")
+		return redirect(url_for('forum.view_forum',forum_id=forum['id']))
+	else:
+		user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
+		if user == None:
+			return render_template("role_assignment.html",
+					forum=forum,user=username,invalid_user=True)
+		r = db.execute(
+				"SELECT role FROM role_assignments WHERE user = ? AND forum = ?;",
+					(username,forum['id'])).fetchone()
+		if not r:
+			assigned_role = ""
+		else:
+			assigned_role = r[0]
+		role = get_user_role(forum['id'], username)
+		if is_bureaucrat(forum['id'], g.user):
+			roles = get_forum_roles(forum['id'])
+			roles.remove("other")
+			roles.add("bureaucrat")
+		else:
+			roles = ["approved"]
+		return render_template("role_assignment.html",
+				forum=forum,user=username,role=role,
+				assigned_role=assigned_role,forum_roles=roles)
 
 def forum_config_page(forum, create=False):
-    db = get_db()
-    if request.method == "POST":
-        name = request.form["name"]
-        desc = request.form["description"]
-        if len(name) > 100 or len(name.strip()) == 0:
-            flash("invalid name")
-            return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
-        elif len(desc) > 6000:
-            flash("invalid description")
-            return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
-        if not create:
-            db.execute("UPDATE forums SET name = ?, description = ? WHERE id = ?",
-                    (name,desc,forum['id']))
-            fid = forum['id']
-        else:
-            cur = db.cursor()
-            cur.execute(
-                "INSERT INTO forums (name,description,parent) VALUES (?,?,?)",
-                    (name,desc,forum['id']))
-            new = cur.lastrowid
-            # creator becomes bureaucrat of new forum
-            db.execute("INSERT INTO role_assignments (role,user,forum) VALUES (?,?,?)",
-                    ("bureaucrat",g.user,new))
-            fid = new
-        db.commit()
-        return redirect(url_for('forum.view_forum',forum_id=fid))
-    else:
-        if create:
-            name = ""
-            desc = ""
-        else:
-            name = forum['name']
-            desc = forum['description']
-        cancel_link = url_for('forum.view_forum',forum_id=forum['id'])
-        return render_template("edit_forum.html",create=create,
-                name=name,description=desc,cancel_link=cancel_link)
+	db = get_db()
+	if request.method == "POST":
+		name = request.form["name"]
+		desc = request.form["description"]
+		if len(name) > 100 or len(name.strip()) == 0:
+			flash("invalid name")
+			return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
+		elif len(desc) > 6000:
+			flash("invalid description")
+			return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
+		if not create:
+			db.execute("UPDATE forums SET name = ?, description = ? WHERE id = ?",
+					(name,desc,forum['id']))
+			fid = forum['id']
+		else:
+			cur = db.cursor()
+			cur.execute(
+				"INSERT INTO forums (name,description,parent) VALUES (?,?,?)",
+					(name,desc,forum['id']))
+			new = cur.lastrowid
+			# creator becomes bureaucrat of new forum
+			db.execute("INSERT INTO role_assignments (role,user,forum) VALUES (?,?,?)",
+					("bureaucrat",g.user,new))
+			fid = new
+		db.commit()
+		return redirect(url_for('forum.view_forum',forum_id=fid))
+	else:
+		if create:
+			name = ""
+			desc = ""
+		else:
+			name = forum['name']
+			desc = forum['description']
+		cancel_link = url_for('forum.view_forum',forum_id=forum['id'])
+		return render_template("edit_forum.html",create=create,
+				name=name,description=desc,cancel_link=cancel_link)
 
 @forum_route("edit",methods=["GET","POST"])
 @requires_bureaucrat
 def edit_forum(forum):
-    return forum_config_page(forum)
+	return forum_config_page(forum)
 
 @forum_route("create",methods=["GET","POST"])
 @requires_permission("p_create_subforum")
 def create_forum(forum):
-    return forum_config_page(forum,create=True)
+	return forum_config_page(forum,create=True)
 
 #@forum_route("unlisted")
 #def view_unlisted(forum):
-#    if not is_admin: abort(403) # why doesn't this fucking work
-#    db = get_db()
-#    unlisted = db.execute(
-#        "SELECT * FROM forums WHERE unlisted = 1 AND parent = ?",(forum['id'],))
-#    return render_template('view_unlisted.html',forum=forum,unlisted=unlisted)
+#	if not is_admin: abort(403) # why doesn't this fucking work
+#	db = get_db()
+#	unlisted = db.execute(
+#		"SELECT * FROM forums WHERE unlisted = 1 AND parent = ?",(forum['id'],))
+#	return render_template('view_unlisted.html',forum=forum,unlisted=unlisted)
 
 @bp.route("/search")
 def search():
-    db = get_db()
-    query = request.args["q"]
-    try:
-        results = db.execute("""
-        SELECT posts.id, highlight(posts_fts, 0, '<mark>', '</mark>') AS 
-            content, posts.thread, posts.author, posts.created, posts.edited, 
-            posts.updated, threads.title AS thread_title
-        FROM posts_fts
-        JOIN posts ON posts_fts.rowid = posts.id
-        JOIN threads ON threads.id = posts.thread
-        JOIN public_posts ON public_posts.id = posts.id
-        WHERE posts_fts MATCH ? AND public_posts.public
-        ORDER BY rank
-        LIMIT 50
-        """, (query,)).fetchall()
-    except OperationalError as e:
-        print(e)
-        flash('your search query was malformed.')
-        return redirect(url_for("forum.not_actual_index"))
-
-    display_thread_id = [ True ] * len(results)
-    last_thread = None
-    for ix, result in enumerate(results):
-        if result["thread"] == last_thread:
-            display_thread_id[ix] = False
-        last_thread = result["thread"]
-    return render_template("search_results.html", results=results, query=query, display_thread_id=display_thread_id)
+	db = get_db()
+	query = request.args["q"]
+	try:
+		results = db.execute("""
+		SELECT posts.id, highlight(posts_fts, 0, '<mark>', '</mark>') AS 
+			content, posts.thread, posts.author, posts.created, posts.edited, 
+			posts.updated, threads.title AS thread_title
+		FROM posts_fts
+		JOIN posts ON posts_fts.rowid = posts.id
+		JOIN threads ON threads.id = posts.thread
+		JOIN public_posts ON public_posts.id = posts.id
+		WHERE posts_fts MATCH ? AND public_posts.public
+		ORDER BY rank
+		LIMIT 50
+		""", (query,)).fetchall()
+	except OperationalError as e:
+		print(e)
+		flash('your search query was malformed.')
+		return redirect(url_for("forum.not_actual_index"))
+
+	display_thread_id = [ True ] * len(results)
+	last_thread = None
+	for ix, result in enumerate(results):
+		if result["thread"] == last_thread:
+			display_thread_id[ix] = False
+		last_thread = result["thread"]
+	return render_template("search_results.html", results=results, query=query, display_thread_id=display_thread_id)
diff --git a/apioforum/fuzzy.py b/apioforum/fuzzy.py
index 8396b8f..6edb649 100644
--- a/apioforum/fuzzy.py
+++ b/apioforum/fuzzy.py
@@ -1,35 +1,35 @@
 # fuzzy datetime things
 
 units = (
-    ("y", "year","years",365*24*60*60), # leap years aren't real
-    ("d", "day","days",24*60*60),
-    ("h", "hour","hours",60*60),
-    ("m", "minute","minutes",60),
-    ("s", "second","seconds",1),
+	("y", "year","years",365*24*60*60), # leap years aren't real
+	("d", "day","days",24*60*60),
+	("h", "hour","hours",60*60),
+	("m", "minute","minutes",60),
+	("s", "second","seconds",1),
 )
 
 from datetime import datetime, timedelta, timezone
 
 def fuzzy(seconds, ago=False):
-    if isinstance(seconds, timedelta):
-        seconds = seconds.total_seconds()
-    elif isinstance(seconds, datetime):
-        seconds = (seconds.replace(tzinfo=timezone.utc) - datetime.now(tz=timezone.utc)).total_seconds()
+	if isinstance(seconds, timedelta):
+		seconds = seconds.total_seconds()
+	elif isinstance(seconds, datetime):
+		seconds = (seconds.replace(tzinfo=timezone.utc) - datetime.now(tz=timezone.utc)).total_seconds()
 
-    components_used = 0
-    fmt = "{}"
-    buf = ""
-    if ago:
-        fmt = "in {}" if seconds > 0 else "{} ago"
-    elif seconds > 0: fmt = "in {}"
-    seconds = abs(seconds)
-    for short, _, _, unit_length in units:
-        if seconds >= unit_length:
-            components_used += 1
-            qty = seconds // unit_length
-            buf += str(int(qty)) + short
-            seconds -= qty * unit_length
-        if components_used == 2: break
-    if not buf: return "now"
+	components_used = 0
+	fmt = "{}"
+	buf = ""
+	if ago:
+		fmt = "in {}" if seconds > 0 else "{} ago"
+	elif seconds > 0: fmt = "in {}"
+	seconds = abs(seconds)
+	for short, _, _, unit_length in units:
+		if seconds >= unit_length:
+			components_used += 1
+			qty = seconds // unit_length
+			buf += str(int(qty)) + short
+			seconds -= qty * unit_length
+		if components_used == 2: break
+	if not buf: return "now"
 
-    return fmt.format(buf)
+	return fmt.format(buf)
diff --git a/apioforum/mdrender.py b/apioforum/mdrender.py
index d9b8ea1..5a50661 100644
--- a/apioforum/mdrender.py
+++ b/apioforum/mdrender.py
@@ -2,26 +2,26 @@ import bleach
 from .csscolors import csscolors
 
 allowed_tags = [
-    'p',
-    'h1',
-    'h2',
-    'h3',
-    'h4',
-    'h5',
-    'h6',
-    'pre',
-    'del',
-    'ins',
-    'mark',
-    'img',
-    'marquee',
-    'pulsate',
-    'sup','sub',
-    'table','thead','tbody','tr','th','td',
-    'details','summary',
-    'hr',
-    'br',
-    
+	'p',
+	'h1',
+	'h2',
+	'h3',
+	'h4',
+	'h5',
+	'h6',
+	'pre',
+	'del',
+	'ins',
+	'mark',
+	'img',
+	'marquee',
+	'pulsate',
+	'sup','sub',
+	'table','thead','tbody','tr','th','td',
+	'details','summary',
+	'hr',
+	'br',
+	
 
 
 ]
@@ -31,9 +31,9 @@ allowed_tags += ("mark" + c for c in csscolors)
 
 allowed_attributes = bleach.sanitizer.ALLOWED_ATTRIBUTES.copy()
 allowed_attributes.update(
-    img=['src','alt','title'],
-    ol=['start'],
-    details=['open'],
+	img=['src','alt','title'],
+	ol=['start'],
+	details=['open'],
 )
 
 allowed_tags.extend(bleach.sanitizer.ALLOWED_TAGS)
@@ -42,14 +42,14 @@ cleaner = bleach.sanitizer.Cleaner(tags=allowed_tags,attributes=allowed_attribut
 
 import markdown
 md = markdown.Markdown(extensions=[
-    'pymdownx.tilde',
-    'pymdownx.caret',
-    'fenced_code',
-    'tables',
-    'pymdownx.details',
+	'pymdownx.tilde',
+	'pymdownx.caret',
+	'fenced_code',
+	'tables',
+	'pymdownx.details',
 ])
 
 def render(text):
-    text = md.reset().convert(text)
-    text = cleaner.clean(text)
-    return text
+	text = md.reset().convert(text)
+	text = cleaner.clean(text)
+	return text
diff --git a/apioforum/permissions.py b/apioforum/permissions.py
index 816936c..f705d08 100644
--- a/apioforum/permissions.py
+++ b/apioforum/permissions.py
@@ -1,5 +1,5 @@
 from flask import (
-    g, redirect, url_for, flash
+	g, redirect, url_for, flash
 )
 import functools
 import click
@@ -7,37 +7,37 @@ from flask.cli import with_appcontext
 from .db import get_db
 
 def is_admin():
-    if g.user_info is None:
-        return False
-    else:
-        return g.user_info['admin'] > 0
+	if g.user_info is None:
+		return False
+	else:
+		return g.user_info['admin'] > 0
 
 def admin_required(view):
-    @functools.wraps(view)
-    def wrapped(**kwargs):
-        if is_admin():
-            return view(**kwargs)
-        else:
-            flash("you must be an admin to do that")
-            return redirect(url_for("index"))
-    return wrapped
+	@functools.wraps(view)
+	def wrapped(**kwargs):
+		if is_admin():
+			return view(**kwargs)
+		else:
+			flash("you must be an admin to do that")
+			return redirect(url_for("index"))
+	return wrapped
 
 @click.command("make_admin")
 @click.argument("username")
 @with_appcontext
 def make_admin(username):
-    """makes a user an admin user"""
-    db = get_db()
-    cur = db.cursor()
-    cur.execute("UPDATE users SET admin = 1 WHERE username = ?",(username,))
-    if cur.rowcount == 0:
-        click.echo("no such user found")
-    else:
-        click.echo("ok")
-    db.commit()
+	"""makes a user an admin user"""
+	db = get_db()
+	cur = db.cursor()
+	cur.execute("UPDATE users SET admin = 1 WHERE username = ?",(username,))
+	if cur.rowcount == 0:
+		click.echo("no such user found")
+	else:
+		click.echo("ok")
+	db.commit()
 
 def init_app(app):
-    app.cli.add_command(make_admin)
-    app.context_processor(lambda: dict(is_admin=is_admin()))
+	app.cli.add_command(make_admin)
+	app.context_processor(lambda: dict(is_admin=is_admin()))
 
 
diff --git a/apioforum/read.py b/apioforum/read.py
index 289b40e..b09a1b2 100644
--- a/apioforum/read.py
+++ b/apioforum/read.py
@@ -2,11 +2,11 @@ from flask import g
 from .db import get_db
 
 def is_read(type, id):
-    if g.user == None:
-        return False
-    db = get_db()
-    read = db.execute("SELECT * FROM read WHERE user = ? AND " + type + " = ?;", (g.user, id)).fetchone()
-    if read == None:
-        return False
-    updated = db.execute("SELECT * FROM " + type + "s WHERE id = ?;", (id,)).fetchone()['updated']
-    return updated == None or read['time'] >= updated
+	if g.user == None:
+		return False
+	db = get_db()
+	read = db.execute("SELECT * FROM read WHERE user = ? AND " + type + " = ?;", (g.user, id)).fetchone()
+	if read == None:
+		return False
+	updated = db.execute("SELECT * FROM " + type + "s WHERE id = ?;", (id,)).fetchone()['updated']
+	return updated == None or read['time'] >= updated
diff --git a/apioforum/roles.py b/apioforum/roles.py
index aa1d239..42ce21c 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -3,95 +3,95 @@ from .db import get_db
 from .permissions import is_admin
 
 permissions = [
-    "p_create_threads",
-    "p_reply_threads",
-    "p_manage_threads",
-    "p_delete_posts",
-    "p_view_threads",
-    "p_vote",
-    "p_create_polls",
-    "p_approve",
-    "p_create_subforum",
-    "p_view_forum"
+	"p_create_threads",
+	"p_reply_threads",
+	"p_manage_threads",
+	"p_delete_posts",
+	"p_view_threads",
+	"p_vote",
+	"p_create_polls",
+	"p_approve",
+	"p_create_subforum",
+	"p_view_forum"
 ]
 
 def get_role_config(forum_id, role):
-    db = get_db()
+	db = get_db()
 
-    fid = forum_id
-    the = None
-    while the == None and fid != None:
-        the = db.execute("""
-            SELECT * FROM role_config 
-            WHERE forum = ? AND role = ?;
-            """, (fid,role)).fetchone()
-        fid = db.execute("""
-            SELECT * FROM forums WHERE id = ?
-            """,(fid,)).fetchone()['parent']
-    if the == None:
-        if role == "other":
-            raise(RuntimeError(
-                "unable to find permissions for role 'other', " +
-                "which should have associated permissions in all contexts."))
-        else:
-            return get_role_config(forum_id, "other")
-    return the
+	fid = forum_id
+	the = None
+	while the == None and fid != None:
+		the = db.execute("""
+			SELECT * FROM role_config 
+			WHERE forum = ? AND role = ?;
+			""", (fid,role)).fetchone()
+		fid = db.execute("""
+			SELECT * FROM forums WHERE id = ?
+			""",(fid,)).fetchone()['parent']
+	if the == None:
+		if role == "other":
+			raise(RuntimeError(
+				"unable to find permissions for role 'other', " +
+				"which should have associated permissions in all contexts."))
+		else:
+			return get_role_config(forum_id, "other")
+	return the
 
 def get_user_role(forum_id, username):
-    db = get_db()
-    user = db.execute('SELECT * FROM users WHERE username = ?',
-            (username,)).fetchone()
-    if user == None: return "other"
-    if user['admin']: return "bureaucrat"
-    
-    fid = forum_id
-    the = None
-    while fid != None:
-        r = db.execute("""
-            SELECT * FROM role_assignments
-            WHERE forum = ? AND user = ?;
-            """,(fid,username)).fetchone()
-        # the user's role is equal to the role assignnment of the closest 
-        # ancestor unless the user's role is "bureaucrat" in any ancestor
-        # in which case, the users role is "bureaucrat"
-        if the == None or (r and r['role'] == "bureaucrat"):
-            the = r
-        fid = db.execute("""
-            SELECT * FROM forums WHERE id = ?
-            """,(fid,)).fetchone()['parent']
-    return the['role'] if the != None else 'other'
+	db = get_db()
+	user = db.execute('SELECT * FROM users WHERE username = ?',
+			(username,)).fetchone()
+	if user == None: return "other"
+	if user['admin']: return "bureaucrat"
+	
+	fid = forum_id
+	the = None
+	while fid != None:
+		r = db.execute("""
+			SELECT * FROM role_assignments
+			WHERE forum = ? AND user = ?;
+			""",(fid,username)).fetchone()
+		# the user's role is equal to the role assignnment of the closest 
+		# ancestor unless the user's role is "bureaucrat" in any ancestor
+		# in which case, the users role is "bureaucrat"
+		if the == None or (r and r['role'] == "bureaucrat"):
+			the = r
+		fid = db.execute("""
+			SELECT * FROM forums WHERE id = ?
+			""",(fid,)).fetchone()['parent']
+	return the['role'] if the != None else 'other'
 
 def get_forum_roles(forum_id):
-    db = get_db()
+	db = get_db()
 
-    ancestors = db.execute("""
-        WITH RECURSIVE fs AS
-            (SELECT * FROM forums WHERE id = ?
-             UNION ALL
-             SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
-        SELECT * FROM fs;
-        """,(forum_id,)).fetchall()
-    configs = []
-    for a in ancestors:
-        configs += db.execute("""
-            SELECT * FROM role_config WHERE forum = ?
-            """,(a['id'],)).fetchall()
-    return set(r['role'] for r in configs)
+	ancestors = db.execute("""
+		WITH RECURSIVE fs AS
+			(SELECT * FROM forums WHERE id = ?
+			 UNION ALL
+			 SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
+		SELECT * FROM fs;
+		""",(forum_id,)).fetchall()
+	configs = []
+	for a in ancestors:
+		configs += db.execute("""
+			SELECT * FROM role_config WHERE forum = ?
+			""",(a['id'],)).fetchall()
+	return set(r['role'] for r in configs)
 
 def has_permission(forum_id, username, permission, login_required=True):
-    db = get_db()
-    forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone()
-    user = db.execute('SELECT * FROM users WHERE username = ?',
-            (username,)).fetchone() if username else None
+	db = get_db()
+	forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone()
+	user = db.execute('SELECT * FROM users WHERE username = ?',
+			(username,)).fetchone() if username else None
 
-    if forum['unlisted'] and not (user and user['admin']): return False
-    if username == None and login_required: return False
+	if forum['unlisted'] and not (user and user['admin']): return False
+	if username == None and login_required: return False
 
-    role = get_user_role(forum_id, username) if username else "other"
-    if role == "bureaucrat": return True
-    config = get_role_config(forum_id, role)
-    return config[permission]
+	role = get_user_role(forum_id, username) if username else "other"
+	if role == "bureaucrat": return True
+	config = get_role_config(forum_id, role)
+	return config[permission]
 
 def is_bureaucrat(forum_id, user):
-    if user == None: return False
-    return get_user_role(forum_id, user) == "bureaucrat"
+	if user == None: return False
+	return get_user_role(forum_id, user) == "bureaucrat"
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index c77ec69..63c7347 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -1,17 +1,17 @@
 body { font-family: sans-serif; word-wrap: break-word; }
 
 :root {
-    --alternating-colour-even: hsl(0, 0%, 96%);
-    --alternating-colour-odd: hsl(0, 0%, 91%);
-    --dark-colour: black;
+	--alternating-colour-even: hsl(0, 0%, 96%);
+	--alternating-colour-odd: hsl(0, 0%, 91%);
+	--dark-colour: black;
 	--gray: darkgray;
-    --light-colour: white;
-    --username-colour: hsl(0, 0%, 25%);
-    --read-colour: hsl(0, 0%, 30%);
-    --red: red;
-    --yellow: yellow;
-    --blue: blue;
-    --visited: #552a8b;
+	--light-colour: white;
+	--username-colour: hsl(0, 0%, 25%);
+	--read-colour: hsl(0, 0%, 30%);
+	--red: red;
+	--yellow: yellow;
+	--blue: blue;
+	--visited: #552a8b;
 }
 
 a { color: var(--blue) }
@@ -21,10 +21,10 @@ a:visited { color: var(--visited); }
 .post:nth-child(even) { background-color: var(--alternating-colour-even) }
 .post:nth-child(odd)  { background-color: var(--alternating-colour-odd) }
 .post {
-    border-left: 1px solid var(--dark-colour);
-    border-right: 1px solid var(--dark-colour);
-    border-top: 1px solid var(--dark-colour);
-    width: 100%;
+	border-left: 1px solid var(--dark-colour);
+	border-right: 1px solid var(--dark-colour);
+	border-top: 1px solid var(--dark-colour);
+	width: 100%;
 	box-sizing: border-box;
 }
 .post:last-of-type { border-bottom: 1px solid var(--dark-colour); }
@@ -34,8 +34,8 @@ a:visited { color: var(--visited); }
 	color: var(--username-colour); 
 }
 .username,nav#navbar .username{
-    font-weight: bold;
-    text-decoration: underline;
+	font-weight: bold;
+	text-decoration: underline;
 }
 .post-heading-em { font-weight: bold; }
 .post-content   * { margin-bottom: 8px; margin-top: 8px; }
@@ -52,9 +52,9 @@ a:visited { color: var(--visited); }
 div.deleted-post { 
 	color:var(--light-colour); 
 	background-color: var(--dark-colour) !important; 
-    border-left: 1px solid var(--dark-colour);
-    border-right: 1px solid var(--dark-colour);
-    border-top: 1px solid var(--dark-colour);
+	border-left: 1px solid var(--dark-colour);
+	border-right: 1px solid var(--dark-colour);
+	border-top: 1px solid var(--dark-colour);
 }
 .deleted-post > .post-heading > * { 
 	color: hsl(0,0%,85%);
@@ -95,22 +95,22 @@ div.deleted-post {
 }
 
 .thread-top-bar, .user-top-bar {
-    margin-bottom: 4px;
+	margin-bottom: 4px;
 }
 
 .thread-top-bar-b {
-    float: right;
-    margin-right: -2px;
+	float: right;
+	margin-right: -2px;
 }
 
 .thread-top-bar-b .tag {
-    font-size: .9rem;
+	font-size: .9rem;
 }
 
 .user_info {
-    border: 1px solid var(--dark-colour);
+	border: 1px solid var(--dark-colour);
 	background-color: var(--alternating-colour-even);
-    width: 100%;
+	width: 100%;
 	padding: 4px;
 }
 .user_bio_quote { width: max-content; max-width: 100% }
@@ -195,9 +195,9 @@ nav#pages .pageno { align-self: center; }
 .listing:nth-child(odd) { background-color: var(--alternating-colour-odd) }
 
 .listing {
-    border-left: 1px solid var(--dark-colour);
-    border-right: 1px solid var(--dark-colour);
-    border-top: 1px solid var(--dark-colour);
+	border-left: 1px solid var(--dark-colour);
+	border-right: 1px solid var(--dark-colour);
+	border-top: 1px solid var(--dark-colour);
 	padding: 10px;
 }
 .listing:last-of-type { border-bottom: 1px solid var(--dark-colour); }
@@ -273,12 +273,12 @@ nav#pages .pageno { align-self: center; }
 }
 
 .actionbutton::before {
-    content: "[";
-    color: var(--gray);
+	content: "[";
+	color: var(--gray);
 }
 .actionbutton::after {
-    content: "]";
-    color: var(--gray);
+	content: "]";
+	color: var(--gray);
 }
 .actionbutton,.actionbutton:visited {
 	color:var(--blue);
@@ -286,32 +286,32 @@ nav#pages .pageno { align-self: center; }
 }
 
 .new-post-box, .forum-desc-box {
-    height:20em;
-    resize:vertical;
-    width:100%;
-    border:1px solid var(--dark-colour);
-    margin-top: 5px;
+	height:20em;
+	resize:vertical;
+	width:100%;
+	border:1px solid var(--dark-colour);
+	margin-top: 5px;
 }
 
 #polloptions {
 	display: block;
 	resize: vertical;
-    border:1px solid var(--dark-colour);
-    margin-top: 5px;
+	border:1px solid var(--dark-colour);
+	margin-top: 5px;
 	height: 5em;
 	width: 100%;
 	font-family: sans-serif;
 }
 
 main {
-    max-width: 80ch;
-    margin: auto;
+	max-width: 80ch;
+	margin: auto;
 }
 
 blockquote {
-    margin-left: 10px;
-    padding-left: 10px;
-    border-left: 3px solid var(--gray);
+	margin-left: 10px;
+	padding-left: 10px;
+	border-left: 3px solid var(--gray);
 }
 
 label { user-select: none; }
@@ -321,23 +321,23 @@ fieldset { margin-bottom: 15px; }
 .warning { color: var(--red); font-weight: bold }
 
 .inline-form {
-    display: inline-block;
+	display: inline-block;
 }
 
 .tag {
-    font-size: .75rem;
-    padding: 1px 3px;
-    border: 1px solid var(--dark-colour);
+	font-size: .75rem;
+	padding: 1px 3px;
+	border: 1px solid var(--dark-colour);
 	white-space: nowrap;
 }
 
 .md table {
-    border: 1px solid var(--gray);
-    border-collapse: collapse;
+	border: 1px solid var(--gray);
+	border-collapse: collapse;
 }
 .md table td,.md table th {
-    border: 1px solid var(--gray);
-    padding: 4px;
+	border: 1px solid var(--gray);
+	padding: 4px;
 }
 
 .role-input, .name-input { width: 12ch; }
@@ -345,16 +345,16 @@ fieldset { margin-bottom: 15px; }
 .thing-id { color: var(--gray); font-size: smaller; font-weight: normal; }
 
 .breadcrumbs {
-    list-style: none;
+	list-style: none;
 }
 
 .breadcrumbs li {
-    display: inline;
+	display: inline;
 }
 
 .breadcrumbs li+li::before {
-    content: "/\00a0";
-    padding: 8px;
+	content: "/\00a0";
+	padding: 8px;
 }
 
 textarea {
diff --git a/apioforum/templates/admin/admin_page.html b/apioforum/templates/admin/admin_page.html
index f48c6c0..fb558bf 100644
--- a/apioforum/templates/admin/admin_page.html
+++ b/apioforum/templates/admin/admin_page.html
@@ -6,9 +6,9 @@
 {% block content %}
 <h2>admins</h2>
 <ul>
-    {% for admin in admins %}
-    <li>{{admin.username}}</li>
-    {% endfor %}
+	{% for admin in admins %}
+	<li>{{admin.username}}</li>
+	{% endfor %}
 </ul>
 <p>this page will have more things on it later, probably</p>
 {% endblock %}
diff --git a/apioforum/templates/auth/login.html b/apioforum/templates/auth/login.html
index 89f490f..361650f 100644
--- a/apioforum/templates/auth/login.html
+++ b/apioforum/templates/auth/login.html
@@ -1,20 +1,20 @@
 {% extends "base.html" %}
 {% block header %}
-    <h1>{% block title %}login{% endblock %}</h1>
+	<h1>{% block title %}login{% endblock %}</h1>
 {% endblock %}
 
 {% block content %}
 <p>log in using an existing account here. if you don't already have an account, <a href="{{url_for('auth.register')}}">register</a> first instead.</p>
 <form method="post">
-    <label for="username">username</label>
-    <input name="username" id="username" required>
-    <br>
-    <label for="password">password</label>
-    <input type="password" name="password" id="password" required>
-    <br>
-    <input type="checkbox" name="keep_logged_in" id="keep_logged_in">
-    <label for="keep_logged_in">keep me logged in</label>
-    <br>
-    <input type="submit" value="login">
+	<label for="username">username</label>
+	<input name="username" id="username" required>
+	<br>
+	<label for="password">password</label>
+	<input type="password" name="password" id="password" required>
+	<br>
+	<input type="checkbox" name="keep_logged_in" id="keep_logged_in">
+	<label for="keep_logged_in">keep me logged in</label>
+	<br>
+	<input type="submit" value="login">
 </form>
 {% endblock %}
diff --git a/apioforum/templates/auth/register.html b/apioforum/templates/auth/register.html
index 082a95b..ed09c56 100644
--- a/apioforum/templates/auth/register.html
+++ b/apioforum/templates/auth/register.html
@@ -1,20 +1,20 @@
 {% extends "base.html" %}
 {% block header %}
-    <h1>{% block title %}register{% endblock %}</h1>
+	<h1>{% block title %}register{% endblock %}</h1>
 {% endblock %}
 
 {% block content %}
 <p>create a new account here. if you already have an account, <a href="{{url_for('auth.login')}}">login</a> instead.</p>
 <form method="post">
-    <label for="username">username</label>
-    <input name="username" id="username" maxlength="20" required>
-    <br>
-    <label for="password">password</label>
-    <input type="password" name="password" id="password" required>
-    <br>
-    <input type="checkbox" name="keep_logged_in" id="keep_logged_in">
-    <label for="keep_logged_in">keep me logged in</label>
-    <br>
-    <input type="submit" value="register">
+	<label for="username">username</label>
+	<input name="username" id="username" maxlength="20" required>
+	<br>
+	<label for="password">password</label>
+	<input type="password" name="password" id="password" required>
+	<br>
+	<input type="checkbox" name="keep_logged_in" id="keep_logged_in">
+	<label for="keep_logged_in">keep me logged in</label>
+	<br>
+	<input type="submit" value="register">
 </form>
 {% endblock %}
diff --git a/apioforum/templates/base.html b/apioforum/templates/base.html
index a202300..ed0a195 100644
--- a/apioforum/templates/base.html
+++ b/apioforum/templates/base.html
@@ -2,15 +2,15 @@
 {% from 'common.html' import disp_user with context %}
 <!DOCTYPE html>
 <html>
-    <head>
-        <title>{%block title %}{% endblock %}</title>
-        <meta name="viewport" content="width=device-width, initial-scale=1">
-        <link rel="stylesheet" href="/static/style.css">
-        <link rel="stylesheet" href="/static/md-colors.css">
-        <link rel="icon" href="//gh0.pw/i/a.ico">
-    </head>
-    <body>
-        <nav aria-label="main" id="navbar">
+	<head>
+		<title>{%block title %}{% endblock %}</title>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<link rel="stylesheet" href="/static/style.css">
+		<link rel="stylesheet" href="/static/md-colors.css">
+		<link rel="icon" href="//gh0.pw/i/a.ico">
+	</head>
+	<body>
+		<nav aria-label="main" id="navbar">
 			<p style="font-family: monospace;"><b><red>ap</red><orange>i</orange><yellow>o</yellow><green>f</green><blue>o</blue><indigo>r</indigo><violet>um</violet></b>&trade;</p>
 			<form class="inline-form" action="/search">
 				<input type="search" placeholder="query" name="q">
@@ -27,7 +27,7 @@
 				{% if is_admin %}
 				<p><a href="{{url_for('admin.admin_page')}}">admin</a></p>
 				{% endif %}
-    				
+					
 				<p>
 					<a href="{{url_for('auth.logout',next=path_for_next)}}">
 						logout
@@ -47,24 +47,24 @@
 				{% endif %}
 
 			</div>
-        </nav>
+		</nav>
 
-        <div class="header">
-        {% block header %}{% endblock %}
-        </div>
+		<div class="header">
+		{% block header %}{% endblock %}
+		</div>
 
-        {% for msg in get_flashed_messages() %}
-            <div class="flashmsg">{{ msg }}</div>
-        {% endfor %}
+		{% for msg in get_flashed_messages() %}
+			<div class="flashmsg">{{ msg }}</div>
+		{% endfor %}
 
-        {% block nmcontent %}
-        <main>
-            {%block content %}{% endblock %}
-        </main>
-        {% endblock %}
-    <script>/* bees */</script>
+		{% block nmcontent %}
+		<main>
+			{%block content %}{% endblock %}
+		</main>
+		{% endblock %}
+	<script>/* bees */</script>
 	<!-- citrons was here -->
 	<!-- Complete hybridisation of various species of wild duck gene pools could result in the extinction of many indigenous waterfowl. -->
-    </body>
+	</body>
 </html>
 
diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html
index fae4b7c..a24272b 100644
--- a/apioforum/templates/common.html
+++ b/apioforum/templates/common.html
@@ -4,8 +4,8 @@
 
 {% macro disp_post(post, buttons=False, forum=None, footer=None) %}
 <div class="post {% if post.deleted %}deleted-post{% endif %}" id="post_{{post.id}}">
-    <div class="post-heading">
-        <span class="post-heading-a">
+	<div class="post-heading">
+		<span class="post-heading-a">
 			{% if not post.deleted %}
 				{{disp_user(post.author)}}
 			{% else %}
@@ -27,12 +27,12 @@
 
 			{{ts(post.created)}}
 
-            {% if post.edited %}
-                (edited {{ts(post.updated)}})
-            {% endif %}
-        </span>
-        <span class="post-heading-b">
-        {% if buttons and not post.deleted %}
+			{% if post.edited %}
+				(edited {{ts(post.updated)}})
+			{% endif %}
+		</span>
+		<span class="post-heading-b">
+		{% if buttons and not post.deleted %}
 			{% if post.author == g.user %}
 				<a class="actionbutton"
 				   href="{{url_for('thread.edit_post',post_id=post.id)}}">edit</a>
@@ -41,25 +41,25 @@
 				<a class="actionbutton"
 				   href="{{url_for('thread.delete_post',post_id=post.id)}}">delete</a>
 			{% endif %}
-            <a class="actionbutton"
-               href="{{url_for('thread.view_post',post_id=post.id)}}">src</a>
-        {% endif %}
-            
+			<a class="actionbutton"
+			   href="{{url_for('thread.view_post',post_id=post.id)}}">src</a>
+		{% endif %}
+			
 		<a class="post-anchor-link" href="{{post_jump(post.id)}}">#{{post.id}}</a>
-        </span>
-    </div>
-    <div class="post-content md">
+		</span>
+	</div>
+	<div class="post-content md">
 		{% if not post.deleted %}
 			{{ post.content|md|safe }}
 		{% else %}
 			this post never existed.
 		{% endif %}
-    </div>
-    {% if footer %}
-    <div class="post-footer">
-        {{ footer }}
-    </div>
-    {% endif %}
+	</div>
+	{% if footer %}
+	<div class="post-footer">
+		{{ footer }}
+	</div>
+	{% endif %}
 </div>
 {% endmacro %}
 
@@ -79,7 +79,7 @@
   {% if href is not none -%}
   href="{{href}}"
   {%- endif -%}>
-    {{-the_tag.name-}}
+	{{-the_tag.name-}}
 </{{el}}>
 {%- endmacro %}
 
@@ -123,9 +123,9 @@
 		<text text-anchor="middle" dominant-baseline="middle" x="11%" y="55%" fill="black" style="font-size:15px">no votes</text>
 	{% else %}
 		{% for opt in poll.options %}
-		    {% set opt_count = opt.num or 0 %}
-		    {% set colour = (loop.index|string + opt.text)|gen_colour %}
-		    {% if opt_count != 0 %}
+			{% set opt_count = opt.num or 0 %}
+			{% set colour = (loop.index|string + opt.text)|gen_colour %}
+			{% if opt_count != 0 %}
 				{% set percentage = 100*(opt_count/total_votes) %}
 				{# todo: do this in css somehow #}
 				{% if opt.text|length > 10 %}
diff --git a/apioforum/templates/config_thread.html b/apioforum/templates/config_thread.html
index 0795ccc..1debe76 100644
--- a/apioforum/templates/config_thread.html
+++ b/apioforum/templates/config_thread.html
@@ -33,22 +33,22 @@
 {% if thread.poll is none %}
 <h2>create poll</h2>
 <form method="post" action="{{url_for('thread.create_poll',thread_id=thread.id)}}">
-    <fieldset>
-        <legend>create poll</legend>
-        <label for="polltitle">question title</label>
-        <input type="title" id="polltitle" name="polltitle">
-        <br>
-        <label for="polloptions">options (one per line)</label>
-        <textarea name="polloptions" id="polloptions"></textarea>
-    </fieldset>
-    <p>important: once a poll is created, you will not be able to modify it except to delete it entirely</p>
-    <input type="submit" value="create">
+	<fieldset>
+		<legend>create poll</legend>
+		<label for="polltitle">question title</label>
+		<input type="title" id="polltitle" name="polltitle">
+		<br>
+		<label for="polloptions">options (one per line)</label>
+		<textarea name="polloptions" id="polloptions"></textarea>
+	</fieldset>
+	<p>important: once a poll is created, you will not be able to modify it except to delete it entirely</p>
+	<input type="submit" value="create">
 </form>
 {% else %}
 <h2>delete poll</h2>
 <p>there is already a poll attached to this thread. you can delete it, which will allow you to create a new one, but this will erase all existing votes and data for the current poll.</p>
 <form action="{{url_for('thread.delete_poll',thread_id=thread.id)}}" method="post">
-    <input type="submit" value="confirm: delete poll">
+	<input type="submit" value="confirm: delete poll">
 </form>
 {% endif %}
 {% endif %}
diff --git a/apioforum/templates/create_thread.html b/apioforum/templates/create_thread.html
index 04b4f42..90d1410 100644
--- a/apioforum/templates/create_thread.html
+++ b/apioforum/templates/create_thread.html
@@ -5,11 +5,11 @@
 
 {% block content %}
 <form method="POST">
-    <label for="title">thread title</label>
-    <input name="title" id="title">
-    <br>
-    <label for="content">thread content</label>
-    <textarea name="content" id="content" class="new-post-box" placeholder="thread content here"></textarea>
-    <input type="submit" value="create">
+	<label for="title">thread title</label>
+	<input name="title" id="title">
+	<br>
+	<label for="content">thread content</label>
+	<textarea name="content" id="content" class="new-post-box" placeholder="thread content here"></textarea>
+	<input type="submit" value="create">
 </form>
 {% endblock %}
diff --git a/apioforum/templates/edit_forum.html b/apioforum/templates/edit_forum.html
index f165676..3c07d1a 100644
--- a/apioforum/templates/edit_forum.html
+++ b/apioforum/templates/edit_forum.html
@@ -5,10 +5,10 @@
 
 {% block content %}
 <form method="POST">
-    <label for="name">forum name</label>
+	<label for="name">forum name</label>
 	<input name="name" id="name" value="{{name}}" placeholder="apioforum" required maxlength="100"/>
-    <br>
-    <label for="description">forum description (markdown enabled)</label>
+	<br>
+	<label for="description">forum description (markdown enabled)</label>
 	<textarea 
 		name="description" 
 		id="description" 
diff --git a/apioforum/templates/search_results.html b/apioforum/templates/search_results.html
index fe016ab..a55dc8a 100644
--- a/apioforum/templates/search_results.html
+++ b/apioforum/templates/search_results.html
@@ -6,23 +6,23 @@
 
 {%block content%}
 <div class="results">
-    {% for result in results %}
-    {% if display_thread_id[loop.index0] %}
-        {% if loop.index0 != 0 %}
-        </div>
-        {% endif %}
-        <h3><a href="{{url_for('thread.view_thread', thread_id=result.thread)}}">
-            {{result.thread_title}}
-        </a></h3>
-        <div class="posts">
-    {% endif %}
-    {{ disp_post(result, False) }}
-    {% endfor %}
+	{% for result in results %}
+	{% if display_thread_id[loop.index0] %}
+		{% if loop.index0 != 0 %}
+		</div>
+		{% endif %}
+		<h3><a href="{{url_for('thread.view_thread', thread_id=result.thread)}}">
+			{{result.thread_title}}
+		</a></h3>
+		<div class="posts">
+	{% endif %}
+	{{ disp_post(result, False) }}
+	{% endfor %}
 
-    {% if results|length > 0 %}
-        </div>
-    {% else %}
-        <p>no results were found for '{{query}}'.</p>
-    {% endif %}
+	{% if results|length > 0 %}
+		</div>
+	{% else %}
+		<p>no results were found for '{{query}}'.</p>
+	{% endif %}
 </div>
 {% endblock %}
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index d37a018..7732b21 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -110,8 +110,8 @@ you do not have permission to create threads in this forum
 	{% for the_tag in avail_tags %}
 
 	<input type="radio" id="tagfilter-{{the_tag.id}}"
-	       name="tagfilter" value="{{the_tag.id}}"
-	       {% if tagfilter_tag.id == the_tag.id %}checked{% endif %}>
+		   name="tagfilter" value="{{the_tag.id}}"
+		   {% if tagfilter_tag.id == the_tag.id %}checked{% endif %}>
 	<label for="tagfilter-{{the_tag.id}}">
 		{{tag(the_tag)}}
 	</label>
diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html
index 6e881cb..fa707a6 100644
--- a/apioforum/templates/view_thread.html
+++ b/apioforum/templates/view_thread.html
@@ -9,93 +9,93 @@
 {% if poll %}
 <p>{{poll.title}}</p>
 <ol>
-    {%for opt in poll.options%}
-    <li value="{{opt.option_idx}}"><i>{{opt.text}}</i>: {{opt.num or 0}} votes</li>
-    {%endfor%}
+	{%for opt in poll.options%}
+	<li value="{{opt.option_idx}}"><i>{{opt.text}}</i>: {{opt.num or 0}} votes</li>
+	{%endfor%}
 </ol>
 {{ vote_meter(poll) }}
 {% endif %}
 <div class="thread-top-bar">
-    <span class="thread-top-bar-a">
-        {% if g.user == thread.creator or has_permission(thread.forum, g.user, "p_manage_threads")  %}
-        <a class="actionbutton" href="{{url_for('thread.config_thread',thread_id=thread.id)}}">configure thread</a>
-        {% endif %}
+	<span class="thread-top-bar-a">
+		{% if g.user == thread.creator or has_permission(thread.forum, g.user, "p_manage_threads")  %}
+		<a class="actionbutton" href="{{url_for('thread.config_thread',thread_id=thread.id)}}">configure thread</a>
+		{% endif %}
 		{% if has_permission(thread.forum, g.user, "p_delete_posts") %}
-        <a class="actionbutton" href="{{url_for('thread.delete_thread',thread_id=thread.id)}}">delete thread</a>
+		<a class="actionbutton" href="{{url_for('thread.delete_thread',thread_id=thread.id)}}">delete thread</a>
 		{% endif %}
-    </span>
-    &nbsp;
-    <span class="thread-top-bar-b">
-        {% for the_tag in tags %}
-            {{ tag(the_tag) }}
-        {% endfor %}
-    </span>
+	</span>
+	&nbsp;
+	<span class="thread-top-bar-b">
+		{% for the_tag in tags %}
+			{{ tag(the_tag) }}
+		{% endfor %}
+	</span>
 </div>
 
 {{ pagination_nav(page,max_pageno,'thread.view_thread',thread_id=thread.id) }}
 <div class="posts">
-    {% for post in posts %}
-        {% if votes[post.id] %}
+	{% for post in posts %}
+		{% if votes[post.id] %}
 
-            {% set vote = votes[post.id] %}        
-            {% set option_idx = vote.option_idx %}
-            
-            {# this is bad but it's going to get refactored anyway #}
-            {% set footer %}
-                {% if vote.is_retraction %}
+			{% set vote = votes[post.id] %}		
+			{% set option_idx = vote.option_idx %}
+			
+			{# this is bad but it's going to get refactored anyway #}
+			{% set footer %}
+				{% if vote.is_retraction %}
 					{% if not post.deleted %}
 						{{post.author}} retracted their vote
 					{% else %}
 						this post retracted a vote
 					{% endif %}
-                {% else %}
-                    {% set option = poll.options[option_idx-1] %}
-                    {% if vote.current %}
-                        {{post.author}} votes for {{option_idx}}: {{option.text}}
-                    {% else %}
+				{% else %}
+					{% set option = poll.options[option_idx-1] %}
+					{% if vote.current %}
+						{{post.author}} votes for {{option_idx}}: {{option.text}}
+					{% else %}
 						{% if not post.deleted %}
 							{{post.author}} voted for {{option_idx}}: {{option.text}}, but later changed their vote
 						{% else %}
 							this post presented a vote that was later changed
 						{% endif %}
-                    {% endif %}
-                {% endif %}
+					{% endif %}
+				{% endif %}
 
-            {% endset %}
+			{% endset %}
 
-            {{ disp_post(post, forum=thread.forum, buttons=True, footer=footer) }}
-            
-        {% else %}
-            {{ disp_post(post, forum=thread.forum, buttons=True) }}
-        {% endif %}
-    {% endfor %}
+			{{ disp_post(post, forum=thread.forum, buttons=True, footer=footer) }}
+			
+		{% else %}
+			{{ disp_post(post, forum=thread.forum, buttons=True) }}
+		{% endif %}
+	{% endfor %}
 </div>
 {% if g.user and has_permission(thread.forum, g.user, "p_reply_threads") %}
 <form class="new-post" action="{{url_for('thread.create_post',thread_id=thread.id)}}" method="POST">
-    <textarea class="new-post-box" placeholder="your post here..." name="content"></textarea>
-    {% if poll and has_permission(thread.forum, g.user, "p_vote") %}
-    <fieldset>
-        <legend>poll: {{poll.title}}</legend>
-        <p>if you want, you can submit a vote along with this post. if you have previously voted
-           on this poll, your previous vote will be changed</p>
+	<textarea class="new-post-box" placeholder="your post here..." name="content"></textarea>
+	{% if poll and has_permission(thread.forum, g.user, "p_vote") %}
+	<fieldset>
+		<legend>poll: {{poll.title}}</legend>
+		<p>if you want, you can submit a vote along with this post. if you have previously voted
+		   on this poll, your previous vote will be changed</p>
 
-        <input type="radio" id="dontvote" name="poll" value="dontvote" checked>
-        <label for="dontvote">do not submit any vote at the moment</label>
+		<input type="radio" id="dontvote" name="poll" value="dontvote" checked>
+		<label for="dontvote">do not submit any vote at the moment</label>
 
-        {% if has_voted %}
-        <br>
-        <input type="radio" id="retractvote" name="poll" value="retractvote">
-        <label for="retractvote">clear current vote</label>
-        {% endif %}
+		{% if has_voted %}
+		<br>
+		<input type="radio" id="retractvote" name="poll" value="retractvote">
+		<label for="retractvote">clear current vote</label>
+		{% endif %}
 
-        {% for opt in poll.options %}
-            <br>
-            <input type="radio" id="option_{{opt.option_idx}}" name="poll" value="{{opt.option_idx}}">
-            <label for="option_{{opt.option_idx}}">#{{opt.option_idx}} - {{opt.text}}</label>
-        {% endfor %}
-    </fieldset>
-    {% endif %}
-    <input type="submit" value="yes">
+		{% for opt in poll.options %}
+			<br>
+			<input type="radio" id="option_{{opt.option_idx}}" name="poll" value="{{opt.option_idx}}">
+			<label for="option_{{opt.option_idx}}">#{{opt.option_idx}} - {{opt.text}}</label>
+		{% endfor %}
+	</fieldset>
+	{% endif %}
+	<input type="submit" value="yes">
 </form>
 {% elif g.user %}
 <p>you do not have permission to reply to this thread</p>
diff --git a/apioforum/thread.py b/apioforum/thread.py
index fdd72e1..9352afb 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -3,8 +3,8 @@
 import itertools, math
 
 from flask import (
-    Blueprint, render_template, abort, request, g, redirect,
-    url_for, flash, jsonify
+	Blueprint, render_template, abort, request, g, redirect,
+	url_for, flash, jsonify
 )
 from .db import get_db
 from .roles import has_permission
@@ -17,403 +17,403 @@ bp = Blueprint("thread", __name__, url_prefix="/thread")
 POSTS_PER_PAGE = 28
 
 def which_page(post_id,return_thread_id=False):
-    # on which page lieth the post in question?
-    # forget not that page numbers employeth a system that has a base of 1.
-    # the
-    # we need impart the knowledgf e into ourselves pertaining to the
-    # number of things
-    # before the thing
-    # yes
-
-    db = get_db()
-    # ASSUMES THAT post ids are consecutive and things
-    # this is probably a reasonable assumption
-
-    thread_id = db.execute('select thread from posts where id = ?',(post_id,)).fetchone()['thread']
-    
-    number_of_things_before_the_thing = db.execute('select count(*) as c, thread as t from posts where thread = ? and id < ?;',(thread_id,post_id)).fetchone()['c']
-
-    
-    page =  1+math.floor(number_of_things_before_the_thing/POSTS_PER_PAGE)
-    if return_thread_id:
-        return page, thread_id
-    else:
-        return page
+	# on which page lieth the post in question?
+	# forget not that page numbers employeth a system that has a base of 1.
+	# the
+	# we need impart the knowledgf e into ourselves pertaining to the
+	# number of things
+	# before the thing
+	# yes
+
+	db = get_db()
+	# ASSUMES THAT post ids are consecutive and things
+	# this is probably a reasonable assumption
+
+	thread_id = db.execute('select thread from posts where id = ?',(post_id,)).fetchone()['thread']
+	
+	number_of_things_before_the_thing = db.execute('select count(*) as c, thread as t from posts where thread = ? and id < ?;',(thread_id,post_id)).fetchone()['c']
+
+	
+	page =  1+math.floor(number_of_things_before_the_thing/POSTS_PER_PAGE)
+	if return_thread_id:
+		return page, thread_id
+	else:
+		return page
 
 def post_jump(post_id,*,external=False):
-    page,thread_id=which_page(post_id,True)
-    return url_for("thread.view_thread",thread_id=thread_id,page=page,_external=external)+"#post_"+str(post_id)
+	page,thread_id=which_page(post_id,True)
+	return url_for("thread.view_thread",thread_id=thread_id,page=page,_external=external)+"#post_"+str(post_id)
 
 @bp.route("/<int:thread_id>")
 @bp.route("/<int:thread_id>/page/<int:page>")
 def view_thread(thread_id,page=1):
-    db = get_db()
-    thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone()
-    if thread is None:
-        abort(404)
-    if not has_permission(thread['forum'], g.user, "p_view_threads", False):
-        abort(403)
-
-    num_posts = db.execute("SELECT count(*) as count FROM posts WHERE posts.thread = ?",(thread_id,)).fetchone()['count']
-    max_pageno = math.ceil(num_posts/POSTS_PER_PAGE)
-    if page < 1:
-        abort(404)
-    elif page > max_pageno and (max_pageno > 0 or page != 1):
-        abort(404)
-
-    posts = db.execute("""
-        SELECT * FROM posts
-        WHERE posts.thread = ?
-        ORDER BY created ASC
-        LIMIT ? OFFSET ?;
-        """,(
-            thread_id,
-            POSTS_PER_PAGE,
-            (page-1)*POSTS_PER_PAGE,
-        )).fetchall()
-
-    tags = db.execute(
-        """SELECT tags.* FROM tags
-        INNER JOIN thread_tags ON thread_tags.tag = tags.id
-        WHERE thread_tags.thread = ?
-        ORDER BY tags.id""",(thread_id,)).fetchall()
-    poll = None
-    votes = None
-    if thread['poll'] is not None:
-        poll_row= db.execute("""
-            SELECT polls.*,total_vote_counts.total_votes FROM polls
-            LEFT OUTER JOIN total_vote_counts ON polls.id = total_vote_counts.poll
-            WHERE polls.id = ?;                
-            """,(thread['poll'],)).fetchone()
-        options = db.execute("""
-            SELECT poll_options.*, vote_counts.num
-            FROM poll_options
-            LEFT OUTER JOIN vote_counts  ON poll_options.poll = vote_counts.poll
-                                        AND poll_options.option_idx = vote_counts.option_idx 
-            WHERE poll_options.poll = ?
-            ORDER BY option_idx asc;
-            """,(poll_row['id'],)).fetchall()
-        poll = {}
-        poll.update(poll_row)
-        poll['options'] = options
-        votes = {}
-        # todo: optimise this somehow
-        for post in posts:
-            if post['vote'] is not None:
-                votes[post['id']] = db.execute("SELECT * FROM votes WHERE id = ?",(post['vote'],)).fetchone()
-
-    if g.user is None or poll is None:
-        has_voted = None
-    else:
-        v = db.execute("SELECT * FROM votes WHERE poll = ? AND user = ? AND current AND NOT is_retraction;",(poll['id'],g.user)).fetchone()
-        has_voted = v is not None
-
-    if g.user != None:
-        db.execute("DELETE FROM read WHERE user = ? AND thread = ?;", (g.user, thread_id))
-        db.execute("INSERT INTO read (user,thread,time) VALUES (?,?,current_timestamp);", (g.user, thread_id))
-        db.commit()
-        
-    return render_template(
-        "view_thread.html",
-        posts=posts,
-        thread=thread,
-        tags=tags,
-        poll=poll,
-        votes=votes,
-        has_voted=has_voted,
-        page=page,
-        max_pageno=max_pageno,
-    )
+	db = get_db()
+	thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone()
+	if thread is None:
+		abort(404)
+	if not has_permission(thread['forum'], g.user, "p_view_threads", False):
+		abort(403)
+
+	num_posts = db.execute("SELECT count(*) as count FROM posts WHERE posts.thread = ?",(thread_id,)).fetchone()['count']
+	max_pageno = math.ceil(num_posts/POSTS_PER_PAGE)
+	if page < 1:
+		abort(404)
+	elif page > max_pageno and (max_pageno > 0 or page != 1):
+		abort(404)
+
+	posts = db.execute("""
+		SELECT * FROM posts
+		WHERE posts.thread = ?
+		ORDER BY created ASC
+		LIMIT ? OFFSET ?;
+		""",(
+			thread_id,
+			POSTS_PER_PAGE,
+			(page-1)*POSTS_PER_PAGE,
+		)).fetchall()
+
+	tags = db.execute(
+		"""SELECT tags.* FROM tags
+		INNER JOIN thread_tags ON thread_tags.tag = tags.id
+		WHERE thread_tags.thread = ?
+		ORDER BY tags.id""",(thread_id,)).fetchall()
+	poll = None
+	votes = None
+	if thread['poll'] is not None:
+		poll_row= db.execute("""
+			SELECT polls.*,total_vote_counts.total_votes FROM polls
+			LEFT OUTER JOIN total_vote_counts ON polls.id = total_vote_counts.poll
+			WHERE polls.id = ?;				
+			""",(thread['poll'],)).fetchone()
+		options = db.execute("""
+			SELECT poll_options.*, vote_counts.num
+			FROM poll_options
+			LEFT OUTER JOIN vote_counts  ON poll_options.poll = vote_counts.poll
+										AND poll_options.option_idx = vote_counts.option_idx 
+			WHERE poll_options.poll = ?
+			ORDER BY option_idx asc;
+			""",(poll_row['id'],)).fetchall()
+		poll = {}
+		poll.update(poll_row)
+		poll['options'] = options
+		votes = {}
+		# todo: optimise this somehow
+		for post in posts:
+			if post['vote'] is not None:
+				votes[post['id']] = db.execute("SELECT * FROM votes WHERE id = ?",(post['vote'],)).fetchone()
+
+	if g.user is None or poll is None:
+		has_voted = None
+	else:
+		v = db.execute("SELECT * FROM votes WHERE poll = ? AND user = ? AND current AND NOT is_retraction;",(poll['id'],g.user)).fetchone()
+		has_voted = v is not None
+
+	if g.user != None:
+		db.execute("DELETE FROM read WHERE user = ? AND thread = ?;", (g.user, thread_id))
+		db.execute("INSERT INTO read (user,thread,time) VALUES (?,?,current_timestamp);", (g.user, thread_id))
+		db.commit()
+		
+	return render_template(
+		"view_thread.html",
+		posts=posts,
+		thread=thread,
+		tags=tags,
+		poll=poll,
+		votes=votes,
+		has_voted=has_voted,
+		page=page,
+		max_pageno=max_pageno,
+	)
 
 def register_vote(thread,pollval):
-    if pollval is None or pollval == 'dontvote':
-        return
-        
-    is_retraction = pollval == 'retractvote'
-
-    if is_retraction:
-        option_idx = None
-    else:
-        option_idx = int(pollval)
-        
-    db = get_db()
-    cur = db.cursor()
-    cur.execute("""
-        UPDATE votes
-        SET current = 0
-        WHERE poll = ? AND user = ?;
-    """,(thread['poll'],g.user))
-
-    cur.execute("""
-        INSERT INTO votes (user,poll,option_idx,time,current,is_retraction)
-        VALUES (?,?,?,current_timestamp,1,?);
-        """,(g.user,thread['poll'],option_idx,is_retraction))
-    vote_id = cur.lastrowid
-    return vote_id
+	if pollval is None or pollval == 'dontvote':
+		return
+		
+	is_retraction = pollval == 'retractvote'
+
+	if is_retraction:
+		option_idx = None
+	else:
+		option_idx = int(pollval)
+		
+	db = get_db()
+	cur = db.cursor()
+	cur.execute("""
+		UPDATE votes
+		SET current = 0
+		WHERE poll = ? AND user = ?;
+	""",(thread['poll'],g.user))
+
+	cur.execute("""
+		INSERT INTO votes (user,poll,option_idx,time,current,is_retraction)
+		VALUES (?,?,?,current_timestamp,1,?);
+		""",(g.user,thread['poll'],option_idx,is_retraction))
+	vote_id = cur.lastrowid
+	return vote_id
 
 @bp.route("/<int:thread_id>/create_poll",methods=["POST"])
 def create_poll(thread_id):
-    fail = redirect(url_for('thread.config_thread',thread_id=thread_id))
-    success = redirect(url_for('thread.view_thread',thread_id=thread_id))
-    err = None
-    db = get_db()
-    thread = db.execute('select * from threads where id = ?',(thread_id,)).fetchone()
-
-    polltitle = request.form.get('polltitle','').strip()
-    polloptions = [q.strip() for q in request.form.get('polloptions','').split("\n") if len(q.strip()) > 0]
-
-    if thread is None:
-        err = "that thread does not exist"
-    elif g.user is None:
-        err = "you need to be logged in to do that"
-    elif g.user != thread['creator'] and \
-            not has_permission(thread['forum'],g.user,"p_manage_threads"):
-        err = "you can only create polls on threads that you own"
-    elif thread['poll'] is not None:
-        err = "a poll already exists for that thread"
-    elif not len(polltitle) > 0:
-        err = "poll title can't be empty"
-    elif len(polloptions) < 2:
-        err = "you must provide at least 2 options"
-    elif not has_permission(thread['forum'], g.user, "p_create_polls"):
-        err = "you do not have permission to do that"
-
-    if err is not None:
-        flash(err)
-        return fail
-    else:
-        cur = db.cursor()
-        cur.execute("INSERT INTO polls (title) VALUES (?)",(polltitle,))
-        pollid = cur.lastrowid
-        cur.execute("UPDATE threads SET poll = ? WHERE threads.id = ?",(pollid,thread_id))
-        cur.executemany(
-            "INSERT INTO poll_options (poll,option_idx,text) VALUES (?,?,?)",
-            zip(itertools.repeat(pollid),itertools.count(1),polloptions)
-        )
-        db.commit()
-        flash("poll created successfully")
-        return success
+	fail = redirect(url_for('thread.config_thread',thread_id=thread_id))
+	success = redirect(url_for('thread.view_thread',thread_id=thread_id))
+	err = None
+	db = get_db()
+	thread = db.execute('select * from threads where id = ?',(thread_id,)).fetchone()
+
+	polltitle = request.form.get('polltitle','').strip()
+	polloptions = [q.strip() for q in request.form.get('polloptions','').split("\n") if len(q.strip()) > 0]
+
+	if thread is None:
+		err = "that thread does not exist"
+	elif g.user is None:
+		err = "you need to be logged in to do that"
+	elif g.user != thread['creator'] and \
+			not has_permission(thread['forum'],g.user,"p_manage_threads"):
+		err = "you can only create polls on threads that you own"
+	elif thread['poll'] is not None:
+		err = "a poll already exists for that thread"
+	elif not len(polltitle) > 0:
+		err = "poll title can't be empty"
+	elif len(polloptions) < 2:
+		err = "you must provide at least 2 options"
+	elif not has_permission(thread['forum'], g.user, "p_create_polls"):
+		err = "you do not have permission to do that"
+
+	if err is not None:
+		flash(err)
+		return fail
+	else:
+		cur = db.cursor()
+		cur.execute("INSERT INTO polls (title) VALUES (?)",(polltitle,))
+		pollid = cur.lastrowid
+		cur.execute("UPDATE threads SET poll = ? WHERE threads.id = ?",(pollid,thread_id))
+		cur.executemany(
+			"INSERT INTO poll_options (poll,option_idx,text) VALUES (?,?,?)",
+			zip(itertools.repeat(pollid),itertools.count(1),polloptions)
+		)
+		db.commit()
+		flash("poll created successfully")
+		return success
 
 @bp.route("/<int:thread_id>/delete_poll",methods=["POST"])
 def delete_poll(thread_id):
-    fail = redirect(url_for('thread.config_thread',thread_id=thread_id))
-    success = redirect(url_for('thread.view_thread',thread_id=thread_id))
-    err = None
-    db = get_db()
-    thread = db.execute('select * from threads where id = ?',(thread_id,)).fetchone()
-
-    if thread is None:
-        err = "that thread does not exist"
-    elif g.user is None:
-        err = "you need to be logged in to do that"
-    elif g.user != thread['creator'] and not \
-            has_permission(thread['forum'], g.user, "p_manage_threads"):
-        err = "you can only delete polls on threads that you own"
-    elif thread['poll'] is None:
-        err = "there is no poll to delete on this thread"
-
-    if err is not None:
-        flash(err)
-        return fail
-    else:
-        pollid = thread['poll']
-        db.execute("UPDATE posts SET vote = NULL WHERE thread = ?",(thread_id,)) # this assumes only max one poll per thread 
-        db.execute("DELETE FROM votes WHERE poll = ?",(pollid,))
-        db.execute("DELETE FROM poll_options WHERE poll = ?",(pollid,))
-        db.execute("UPDATE THREADS set poll = NULL WHERE id = ?",(thread_id,))
-        db.execute("DELETE FROM polls WHERE id = ?",(pollid,))
-        db.commit()
-        flash("poll deleted successfully")
-        return success
-        
+	fail = redirect(url_for('thread.config_thread',thread_id=thread_id))
+	success = redirect(url_for('thread.view_thread',thread_id=thread_id))
+	err = None
+	db = get_db()
+	thread = db.execute('select * from threads where id = ?',(thread_id,)).fetchone()
+
+	if thread is None:
+		err = "that thread does not exist"
+	elif g.user is None:
+		err = "you need to be logged in to do that"
+	elif g.user != thread['creator'] and not \
+			has_permission(thread['forum'], g.user, "p_manage_threads"):
+		err = "you can only delete polls on threads that you own"
+	elif thread['poll'] is None:
+		err = "there is no poll to delete on this thread"
+
+	if err is not None:
+		flash(err)
+		return fail
+	else:
+		pollid = thread['poll']
+		db.execute("UPDATE posts SET vote = NULL WHERE thread = ?",(thread_id,)) # this assumes only max one poll per thread 
+		db.execute("DELETE FROM votes WHERE poll = ?",(pollid,))
+		db.execute("DELETE FROM poll_options WHERE poll = ?",(pollid,))
+		db.execute("UPDATE THREADS set poll = NULL WHERE id = ?",(thread_id,))
+		db.execute("DELETE FROM polls WHERE id = ?",(pollid,))
+		db.commit()
+		flash("poll deleted successfully")
+		return success
+		
 @bp.route("/<int:thread_id>/create_post", methods=("POST",))
 def create_post(thread_id):
-    if g.user is None:
-        flash("you need to log in before you can post")
-    db = get_db()
-    content = request.form['content']
-    thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone()
-    if len(content.strip()) == 0:
-        flash("you cannot post an empty message")
-    elif not thread:
-        flash("that thread does not exist")
-    elif not has_permission(thread['forum'], g.user, "p_reply_threads"):
-        flash("you do not have permission to do this")
-    elif not has_permission(thread['forum'], g.user, "p_view_threads"):
-        flash("you do not have permission to do this")
-    elif not has_permission(thread['forum'], g.user, "p_vote") \
-            and 'poll' in request.form:
-        flash("you do not have permission to do this")
-    else:
-        vote_id = None
-        if thread['poll'] is not None:
-            pollval = request.form.get('poll')
-            try:
-                vote_id = register_vote(thread,pollval)
-            except ValueError:
-                flash("invalid poll form value")
-                return redirect(url_for('thread.view_thread',thread_id=thread_id))
-
-        cur = db.cursor()
-        cur.execute("""
-            INSERT INTO posts (thread,author,content,created,vote)
-            VALUES (?,?,?,current_timestamp,?);
-            """,(thread_id,g.user,content,vote_id))
-        post_id = cur.lastrowid
-        cur.execute(
-            "UPDATE threads SET updated = current_timestamp WHERE id = ?;",
-            (thread_id,)
-        )
-        db.commit()
-        forum.set_updated(thread['forum'])
-        post = db.execute("select * from posts where id = ?",(post_id,)).fetchone()
-        webhooks.do_webhooks_post(thread['forum'],post)
-        flash("post posted postfully")
-        return redirect(post_jump(post_id))
-    return redirect(url_for('thread.view_thread',thread_id=thread_id))
+	if g.user is None:
+		flash("you need to log in before you can post")
+	db = get_db()
+	content = request.form['content']
+	thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone()
+	if len(content.strip()) == 0:
+		flash("you cannot post an empty message")
+	elif not thread:
+		flash("that thread does not exist")
+	elif not has_permission(thread['forum'], g.user, "p_reply_threads"):
+		flash("you do not have permission to do this")
+	elif not has_permission(thread['forum'], g.user, "p_view_threads"):
+		flash("you do not have permission to do this")
+	elif not has_permission(thread['forum'], g.user, "p_vote") \
+			and 'poll' in request.form:
+		flash("you do not have permission to do this")
+	else:
+		vote_id = None
+		if thread['poll'] is not None:
+			pollval = request.form.get('poll')
+			try:
+				vote_id = register_vote(thread,pollval)
+			except ValueError:
+				flash("invalid poll form value")
+				return redirect(url_for('thread.view_thread',thread_id=thread_id))
+
+		cur = db.cursor()
+		cur.execute("""
+			INSERT INTO posts (thread,author,content,created,vote)
+			VALUES (?,?,?,current_timestamp,?);
+			""",(thread_id,g.user,content,vote_id))
+		post_id = cur.lastrowid
+		cur.execute(
+			"UPDATE threads SET updated = current_timestamp WHERE id = ?;",
+			(thread_id,)
+		)
+		db.commit()
+		forum.set_updated(thread['forum'])
+		post = db.execute("select * from posts where id = ?",(post_id,)).fetchone()
+		webhooks.do_webhooks_post(thread['forum'],post)
+		flash("post posted postfully")
+		return redirect(post_jump(post_id))
+	return redirect(url_for('thread.view_thread',thread_id=thread_id))
 
 @bp.route("/delete_post/<int:post_id>", methods=["GET","POST"])
 def delete_post(post_id):
-    db = get_db()
-    post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone()
-    thread = db.execute("SELECT * FROM threads WHERE id = ?",(post['thread'],)).fetchone()
-    if post is None:
-        flash("that post doesn't exist")
-        return redirect("/")
-    if post['author'] != g.user and not has_permission(thread['forum'], g.user, "p_delete_posts"):
-        flash("you do not have permission to do that")
-        return redirect(url_for("thread.view_thread",thread_id=post["thread"]))
-    if request.method == "POST":
-        db.execute("""
-            UPDATE posts SET 
-                content = '',
-                deleted = 1
-            WHERE id = ?""",(post_id,))
-        db.commit()
-        flash("post deleted deletedly")
-        return redirect(url_for("thread.view_thread",thread_id=post["thread"]))
-    else:
-        return render_template("delete_post.html",post=post)
-        
+	db = get_db()
+	post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone()
+	thread = db.execute("SELECT * FROM threads WHERE id = ?",(post['thread'],)).fetchone()
+	if post is None:
+		flash("that post doesn't exist")
+		return redirect("/")
+	if post['author'] != g.user and not has_permission(thread['forum'], g.user, "p_delete_posts"):
+		flash("you do not have permission to do that")
+		return redirect(url_for("thread.view_thread",thread_id=post["thread"]))
+	if request.method == "POST":
+		db.execute("""
+			UPDATE posts SET 
+				content = '',
+				deleted = 1
+			WHERE id = ?""",(post_id,))
+		db.commit()
+		flash("post deleted deletedly")
+		return redirect(url_for("thread.view_thread",thread_id=post["thread"]))
+	else:
+		return render_template("delete_post.html",post=post)
+		
 @bp.route("/delete_thread/<int:thread_id>", methods=["GET","POST"])
 def delete_thread(thread_id):
-    db = get_db()
-    thread = db.execute("SELECT * FROM threads WHERE id = ?",(thread_id,)).fetchone()
-    if thread is None:
-        flash("that thread doesn't exist")
-        return redirect("/")
-    if not has_permission(thread['forum'], g.user, "p_delete_posts"):
-        flash("you do not have permission to do that")
-        return redirect(url_for("thread.view_thread",thread_id=thread_id))
-    if request.method == "POST":
-        db.execute("DELETE FROM posts WHERE thread = ?",(thread_id,))
-        db.execute("DELETE FROM threads WHERE id = ?",(thread_id,))
-        db.commit()
-        flash("thread deleted deletedly")
-        return redirect(url_for("forum.view_forum",forum_id=thread['forum']))
-    else:
-        count = db.execute("SELECT num_replies FROM number_of_posts WHERE thread = ?",
-                (thread_id,)).fetchone()[0]
-        return render_template("delete_thread.html",thread=thread,post_count=count)
-        
+	db = get_db()
+	thread = db.execute("SELECT * FROM threads WHERE id = ?",(thread_id,)).fetchone()
+	if thread is None:
+		flash("that thread doesn't exist")
+		return redirect("/")
+	if not has_permission(thread['forum'], g.user, "p_delete_posts"):
+		flash("you do not have permission to do that")
+		return redirect(url_for("thread.view_thread",thread_id=thread_id))
+	if request.method == "POST":
+		db.execute("DELETE FROM posts WHERE thread = ?",(thread_id,))
+		db.execute("DELETE FROM threads WHERE id = ?",(thread_id,))
+		db.commit()
+		flash("thread deleted deletedly")
+		return redirect(url_for("forum.view_forum",forum_id=thread['forum']))
+	else:
+		count = db.execute("SELECT num_replies FROM number_of_posts WHERE thread = ?",
+				(thread_id,)).fetchone()[0]
+		return render_template("delete_thread.html",thread=thread,post_count=count)
+		
 
 @bp.route("/edit_post/<int:post_id>",methods=["GET","POST"])
 def edit_post(post_id):
-    db = get_db()
-    post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone()
-    if post is None:
-        flash("that post doesn't exist")
-        return redirect(url_for('index'))
-
-    if post['author'] != g.user:
-        flash("you can only edit posts that you created")
-        return redirect(url_for("thread.view_thread",thread_id=post['thread']))
-    # note: i am writing this while i am very tired, so probably
-    # come back and test this properly later
-    if request.method == "POST":
-        err = None 
-        newcontent = request.form['newcontent']
-        if len(newcontent.strip()) == 0:
-            err="post contents can't be empty"
-        print(err)
-        if err is None:
-            db.execute(
-                "UPDATE posts SET content = ?, edited = 1, updated = current_timestamp WHERE id = ?",(newcontent,post_id))
-            db.commit()
-            flash("post edited editiously")
-            return redirect(post_jump(post_id))
-        else:
-            flash(err)
-    return render_template("edit_post.html",post=post)
+	db = get_db()
+	post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone()
+	if post is None:
+		flash("that post doesn't exist")
+		return redirect(url_for('index'))
+
+	if post['author'] != g.user:
+		flash("you can only edit posts that you created")
+		return redirect(url_for("thread.view_thread",thread_id=post['thread']))
+	# note: i am writing this while i am very tired, so probably
+	# come back and test this properly later
+	if request.method == "POST":
+		err = None 
+		newcontent = request.form['newcontent']
+		if len(newcontent.strip()) == 0:
+			err="post contents can't be empty"
+		print(err)
+		if err is None:
+			db.execute(
+				"UPDATE posts SET content = ?, edited = 1, updated = current_timestamp WHERE id = ?",(newcontent,post_id))
+			db.commit()
+			flash("post edited editiously")
+			return redirect(post_jump(post_id))
+		else:
+			flash(err)
+	return render_template("edit_post.html",post=post)
 
 @bp.route("/view_post/<int:post_id>")
 def view_post(post_id):
-    db = get_db()
-    post = db.execute("""
-        SELECT p.*,h.f_id FROM posts p
-        INNER JOIN forum_thread_of_post h ON p.id = h.p_id
-        WHERE p.id = ?;
-        """,(post_id,)).fetchone()
-
-    if post is None:
-        abort(404)
-
-    if not has_permission(post['f_id'], g.user, "p_view_threads", False):
-        abort(403)
-
-    return render_template("view_post.html",post=post)
-    
-    
-            
+	db = get_db()
+	post = db.execute("""
+		SELECT p.*,h.f_id FROM posts p
+		INNER JOIN forum_thread_of_post h ON p.id = h.p_id
+		WHERE p.id = ?;
+		""",(post_id,)).fetchone()
+
+	if post is None:
+		abort(404)
+
+	if not has_permission(post['f_id'], g.user, "p_view_threads", False):
+		abort(403)
+
+	return render_template("view_post.html",post=post)
+	
+	
+			
 @bp.route("/<int:thread_id>/config",methods=["GET","POST"])
 def config_thread(thread_id):
-    db = get_db()
-    thread = db.execute("select * from threads where id = ?",(thread_id,)).fetchone()
-    thread_tags = [r['tag'] for r in db.execute("select tag from thread_tags where thread = ?",(thread_id,)).fetchall()]
-    avail_tags = get_avail_tags(thread['forum'])
-    err = None
-    if g.user is None:
-        err = "you need to be logged in to do that"
-    elif not has_permission(thread['forum'], g.user, "p_view_threads"):
-        err = "you do not have permission to do that"
-    elif g.user != thread['creator'] and not has_permission(thread['forum'], g.user, "p_manage_threads"):
-        err = "you can only configure threads that you own"
-
-    if err is not None:
-        flash(err)
-        return redirect(url_for("thread.view_thread",thread_id=thread_id))
-
-    if request.method == "POST":
-        err = []
-        if request.form['title'] != thread['title']:
-            title = request.form['title']
-            if len(title.strip()) == 0:
-                err.append("title can't be empty")
-            else:
-                db.execute("update threads set title = ? where id = ?;",(title,thread_id))
-                flash("title updated successfully")
-                db.commit()
-        changed = False
-        for avail_tag in avail_tags:
-            tagid = avail_tag['id']
-            current = tagid in thread_tags
-            wanted  = f'tag_{tagid}' in request.form
-            if wanted and not current:
-                db.execute("insert into thread_tags (thread, tag) values (?,?)",(thread_id,tagid))
-                changed = True
-            elif current and not wanted:
-                db.execute("delete from thread_tags where thread = ? and tag = ?",(thread_id,tagid))
-                changed = True
-        if changed:
-            db.commit()
-            flash("tags updated successfully")
-
-        if len(err) > 0:
-            for e in err:
-                flash(e)
-        else:
-            return redirect(url_for("thread.view_thread",thread_id=thread_id))
-            
-
-    return render_template("config_thread.html", thread=thread,thread_tags=thread_tags,avail_tags=avail_tags)
-    
+	db = get_db()
+	thread = db.execute("select * from threads where id = ?",(thread_id,)).fetchone()
+	thread_tags = [r['tag'] for r in db.execute("select tag from thread_tags where thread = ?",(thread_id,)).fetchall()]
+	avail_tags = get_avail_tags(thread['forum'])
+	err = None
+	if g.user is None:
+		err = "you need to be logged in to do that"
+	elif not has_permission(thread['forum'], g.user, "p_view_threads"):
+		err = "you do not have permission to do that"
+	elif g.user != thread['creator'] and not has_permission(thread['forum'], g.user, "p_manage_threads"):
+		err = "you can only configure threads that you own"
+
+	if err is not None:
+		flash(err)
+		return redirect(url_for("thread.view_thread",thread_id=thread_id))
+
+	if request.method == "POST":
+		err = []
+		if request.form['title'] != thread['title']:
+			title = request.form['title']
+			if len(title.strip()) == 0:
+				err.append("title can't be empty")
+			else:
+				db.execute("update threads set title = ? where id = ?;",(title,thread_id))
+				flash("title updated successfully")
+				db.commit()
+		changed = False
+		for avail_tag in avail_tags:
+			tagid = avail_tag['id']
+			current = tagid in thread_tags
+			wanted  = f'tag_{tagid}' in request.form
+			if wanted and not current:
+				db.execute("insert into thread_tags (thread, tag) values (?,?)",(thread_id,tagid))
+				changed = True
+			elif current and not wanted:
+				db.execute("delete from thread_tags where thread = ? and tag = ?",(thread_id,tagid))
+				changed = True
+		if changed:
+			db.commit()
+			flash("tags updated successfully")
+
+		if len(err) > 0:
+			for e in err:
+				flash(e)
+		else:
+			return redirect(url_for("thread.view_thread",thread_id=thread_id))
+			
+
+	return render_template("config_thread.html", thread=thread,thread_tags=thread_tags,avail_tags=avail_tags)
+	
diff --git a/apioforum/user.py b/apioforum/user.py
index 8fb59ab..cead2fe 100644
--- a/apioforum/user.py
+++ b/apioforum/user.py
@@ -2,7 +2,7 @@
 POSTS_PER_PAGE = 28
 
 from flask import (
-    Blueprint, render_template, abort, g, flash, redirect, url_for, request
+	Blueprint, render_template, abort, g, flash, redirect, url_for, request
 )
 
 from werkzeug.security import check_password_hash, generate_password_hash
@@ -10,72 +10,72 @@ from .db import get_db
 import math
 
 bp = Blueprint("user", __name__, url_prefix="/user")
-    
+	
 @bp.route("/<username>")
 @bp.route("/<username>/page/<int:page>")
 def view_user(username, page=1):
-    if page < 1:
-        abort(400)
-    
-    db = get_db()
-    user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
+	if page < 1:
+		abort(400)
+	
+	db = get_db()
+	user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
    
-    if user is None:
-        abort(404)
-    posts = db.execute("""
-        SELECT * FROM posts
-        JOIN public_posts ON public_posts.id = posts.id
-        WHERE author = ? AND deleted = 0 AND public_posts.public
-        ORDER BY created DESC
-        LIMIT ? OFFSET ?;""",(username,POSTS_PER_PAGE,(page-1)*POSTS_PER_PAGE,)).fetchall()
-    num_posts = db.execute("""
-    	SELECT count(*) as count FROM posts
-    	JOIN public_posts ON public_posts.id = posts.id
-        WHERE author = ? AND public_posts.public;
-        """,(username,)).fetchone()['count']
-    max_pageno = math.ceil(num_posts/POSTS_PER_PAGE)
-    return render_template(
-        "view_user.html",
-        user=user,
-        posts=posts,
-        page=page,
-        max_pageno=max_pageno,
-    )
+	if user is None:
+		abort(404)
+	posts = db.execute("""
+		SELECT * FROM posts
+		JOIN public_posts ON public_posts.id = posts.id
+		WHERE author = ? AND deleted = 0 AND public_posts.public
+		ORDER BY created DESC
+		LIMIT ? OFFSET ?;""",(username,POSTS_PER_PAGE,(page-1)*POSTS_PER_PAGE,)).fetchall()
+	num_posts = db.execute("""
+		SELECT count(*) as count FROM posts
+		JOIN public_posts ON public_posts.id = posts.id
+		WHERE author = ? AND public_posts.public;
+		""",(username,)).fetchone()['count']
+	max_pageno = math.ceil(num_posts/POSTS_PER_PAGE)
+	return render_template(
+		"view_user.html",
+		user=user,
+		posts=posts,
+		page=page,
+		max_pageno=max_pageno,
+	)
 
 @bp.route("/<username>/edit", methods=["GET","POST"])
 def edit_user(username):
-    db = get_db()
-    user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
-    if user is None:
-        abort(404)
-    if username != g.user:
-        flash("you cannot modify other people")
-        return redirect(url_for("user.view_user",username=username))
+	db = get_db()
+	user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
+	if user is None:
+		abort(404)
+	if username != g.user:
+		flash("you cannot modify other people")
+		return redirect(url_for("user.view_user",username=username))
 
-    if request.method == "POST":
-        err = []
-        if len(request.form['new_password']) > 0:
-            if not check_password_hash(user['password'],request.form['password']):
-                err.append("entered password does not match current password")
-            else:
-                db.execute("update users set password = ? where username = ?", 
-                        (generate_password_hash(request.form["new_password"]), username))
-                db.commit()
-                flash("password changed changefully")
-        if request.form['bio'] != user['bio']:
-            if len(request.form['bio'].strip()) == 0:
-                err.append("please submit nonempty bio")
-            elif len(request.form['bio']) > 4500:
-                err.append("bio is too long!!")
-            else:
-                db.execute("update users set bio = ? where username = ?", (request.form['bio'], username))
-                db.commit()
-                flash("bio updated successfully")
+	if request.method == "POST":
+		err = []
+		if len(request.form['new_password']) > 0:
+			if not check_password_hash(user['password'],request.form['password']):
+				err.append("entered password does not match current password")
+			else:
+				db.execute("update users set password = ? where username = ?", 
+						(generate_password_hash(request.form["new_password"]), username))
+				db.commit()
+				flash("password changed changefully")
+		if request.form['bio'] != user['bio']:
+			if len(request.form['bio'].strip()) == 0:
+				err.append("please submit nonempty bio")
+			elif len(request.form['bio']) > 4500:
+				err.append("bio is too long!!")
+			else:
+				db.execute("update users set bio = ? where username = ?", (request.form['bio'], username))
+				db.commit()
+				flash("bio updated successfully")
 
-        if len(err) > 0:
-            for e in err:
-                flash(e)
-        else:
-            return redirect(url_for("user.view_user",username=username))
-        
-    return render_template("user_settings.html",user=user)
+		if len(err) > 0:
+			for e in err:
+				flash(e)
+		else:
+			return redirect(url_for("user.view_user",username=username))
+		
+	return render_template("user_settings.html",user=user)
diff --git a/apioforum/util.py b/apioforum/util.py
index 64bdf20..8f836d1 100644
--- a/apioforum/util.py
+++ b/apioforum/util.py
@@ -5,12 +5,12 @@ import hashlib
 
 # same algorithm as xep-0392
 def gen_colour(s):
-    b=s.encode("utf-8")
-    h=hashlib.sha1(b)
-    two_bytes=h.digest()[:2]
-    val = int.from_bytes(two_bytes, 'little')
-    angle = 360*(val/65536)
-    col = hsluv.hsluv_to_hex([angle, 80, 70])
-    return col
-    
-    
+	b=s.encode("utf-8")
+	h=hashlib.sha1(b)
+	two_bytes=h.digest()[:2]
+	val = int.from_bytes(two_bytes, 'little')
+	angle = 360*(val/65536)
+	col = hsluv.hsluv_to_hex([angle, 80, 70])
+	return col
+	
+	
diff --git a/apioforum/webhooks.py b/apioforum/webhooks.py
index f7387be..a09f951 100644
--- a/apioforum/webhooks.py
+++ b/apioforum/webhooks.py
@@ -6,211 +6,211 @@ from flask import url_for, flash
 
 
 def abridge(text,maxlen=20):
-    if len(text) > maxlen+3:
-        return text[:maxlen]+"..."
-    else:
-        return text
+	if len(text) > maxlen+3:
+		return text[:maxlen]+"..."
+	else:
+		return text
 
 webhook_types = {}
 def webhook_type(t):
-    def inner(cls):
-        webhook_types[t] = cls
-        return cls
-    return inner
+	def inner(cls):
+		webhook_types[t] = cls
+		return cls
+	return inner
 
 class WebhookType(abc.ABC):
-    def __init__(self, url, wh_id):
-        self.url = url
-        self.wh_id = wh_id
+	def __init__(self, url, wh_id):
+		self.url = url
+		self.wh_id = wh_id
 
-    @abc.abstractmethod
-    def on_new_thread(self,thread):
-        pass
-    @abc.abstractmethod
-    def on_new_post(self,post):
-        pass
+	@abc.abstractmethod
+	def on_new_thread(self,thread):
+		pass
+	@abc.abstractmethod
+	def on_new_post(self,post):
+		pass
 
 def get_webhooks(forum_id):
-    db = get_db()
-    # todo inheritance (if needed)
-    webhooks = db.execute("select * from webhooks where webhooks.forum = ?;",(forum_id,)).fetchall()
-
-    for wh in webhooks:
-        wh_type = wh['type']
-        if wh_type not in webhook_types:
-            print(f"unknown webhook type {wh_type}")
-            continue
-        wh_url = wh['url']
-        wo = webhook_types[wh_type](wh_url, wh['id'])
-        yield wo
+	db = get_db()
+	# todo inheritance (if needed)
+	webhooks = db.execute("select * from webhooks where webhooks.forum = ?;",(forum_id,)).fetchall()
+
+	for wh in webhooks:
+		wh_type = wh['type']
+		if wh_type not in webhook_types:
+			print(f"unknown webhook type {wh_type}")
+			continue
+		wh_url = wh['url']
+		wo = webhook_types[wh_type](wh_url, wh['id'])
+		yield wo
 
 def do_webhooks_thread(forum_id,thread):
-    for wh in get_webhooks(forum_id):
-        try:
-            wh.on_new_thread(thread)
-        except Exception as e:
-            #raise e
-            flash(f"error executing webhook with id {wh.wh_id}")
+	for wh in get_webhooks(forum_id):
+		try:
+			wh.on_new_thread(thread)
+		except Exception as e:
+			#raise e
+			flash(f"error executing webhook with id {wh.wh_id}")
 
 def do_webhooks_post(forum_id,post):
-    for wh in get_webhooks(forum_id):
-        try:
-            wh.on_new_post(post)
-        except Exception as e:
-            #raise e
-            flash(f"error executing webhook with id {wh.wh_id}")
+	for wh in get_webhooks(forum_id):
+		try:
+			wh.on_new_post(post)
+		except Exception as e:
+			#raise e
+			flash(f"error executing webhook with id {wh.wh_id}")
 
 
 @webhook_type("fake")
 class FakeWebhook(WebhookType):
-    def on_new_post(self, post):
-        print(f'fake wh {self.url} post {post["id"]}')
-    def on_new_thread(self, thread):
-        print(f'fake wh {self.url} thread {thread["id"]}')
+	def on_new_post(self, post):
+		print(f'fake wh {self.url} post {post["id"]}')
+	def on_new_thread(self, thread):
+		print(f'fake wh {self.url} thread {thread["id"]}')
 
 @webhook_type("discord")
 class DiscordWebhook(WebhookType):
-    def send(self,payload):
-        headers = {
-            "User-Agent":"apioforum (https://g.gh0.pw/apioforum, v0.0)",
-            "Content-Type":"application/json",
-        }
-        req = urllib.request.Request(
-            self.url,
-            json.dumps(payload).encode("utf-8"),
-            headers
-        )
-        # todo: read response and things
-        urllib.request.urlopen(req)
-        #try:
-        #    res = urllib.request.urlopen(req)
-        #except urllib.error.HTTPError as e:
-        #    print(f"error {e.code} {e.read()}")
-        #else:
-        #    print(f"succ {res.read()}")
-
-    @staticmethod
-    def field(name,value):
-        return {"name":name,"value":value,"inline":True}
-
-    def on_new_thread(self,thread):
-        f = self.field
-        db = get_db()
-        forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone()
-        username = thread['creator']
-        userpage = url_for('user.view_user',username=username,_external=True)
-
-        forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True)
-
-        post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone()
-
-        payload = {
-            "username":"apioforum",
-            "avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png",
-            "embeds":[
-                {
-                    "title":"new thread: "+thread['title'],
-                    "description":abridge(post['content']),
-                    "url": url_for('thread.view_thread',thread_id=thread['id'],_external=True),
-                    "color": 0xff00ff,
-                    "fields":[
-                        f('author',f"[{username}]({userpage})"),
-                        f('forum',f"[{forum['name']}]({forumpage})"),
-                    ],
-                    "footer":{
-                        "text":thread['created'].isoformat(' '),
-                    },
-                },
-            ],
-        }
-        self.send(payload)
-
-    def on_new_post(self,post):
-        from .thread import post_jump
-        f = self.field
-        db = get_db()
-
-        thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone()
-        threadpage = url_for('thread.view_thread',thread_id=thread['id'],_external=True)
-
-        forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone()
-        forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True)
-
-        username = post['author']
-        userpage = url_for('user.view_user',username=username,_external=True)
-
-        payload = {
-            "username":"apioforum",
-            "avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png",
-            "embeds":[
-                {
-                    "title":"re: "+thread['title'],
-                    "description":abridge(post['content']),
-                    "url": post_jump(post['id'],external=True),
-                    "color": 0x00ffff,
-                    "fields":[
-                        f('author',f"[{username}]({userpage})"),
-                        f('thread',f"[{thread['title']}]({threadpage})"),
-                        f('forum',f"[{forum['name']}]({forumpage})"),
-                    ],
-                    "footer":{
-                        "text":post['created'].isoformat(' '),
-                    },
-                },
-            ],
-        }
-        self.send(payload)
+	def send(self,payload):
+		headers = {
+			"User-Agent":"apioforum (https://g.gh0.pw/apioforum, v0.0)",
+			"Content-Type":"application/json",
+		}
+		req = urllib.request.Request(
+			self.url,
+			json.dumps(payload).encode("utf-8"),
+			headers
+		)
+		# todo: read response and things
+		urllib.request.urlopen(req)
+		#try:
+		#	res = urllib.request.urlopen(req)
+		#except urllib.error.HTTPError as e:
+		#	print(f"error {e.code} {e.read()}")
+		#else:
+		#	print(f"succ {res.read()}")
+
+	@staticmethod
+	def field(name,value):
+		return {"name":name,"value":value,"inline":True}
+
+	def on_new_thread(self,thread):
+		f = self.field
+		db = get_db()
+		forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone()
+		username = thread['creator']
+		userpage = url_for('user.view_user',username=username,_external=True)
+
+		forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True)
+
+		post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone()
+
+		payload = {
+			"username":"apioforum",
+			"avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png",
+			"embeds":[
+				{
+					"title":"new thread: "+thread['title'],
+					"description":abridge(post['content']),
+					"url": url_for('thread.view_thread',thread_id=thread['id'],_external=True),
+					"color": 0xff00ff,
+					"fields":[
+						f('author',f"[{username}]({userpage})"),
+						f('forum',f"[{forum['name']}]({forumpage})"),
+					],
+					"footer":{
+						"text":thread['created'].isoformat(' '),
+					},
+				},
+			],
+		}
+		self.send(payload)
+
+	def on_new_post(self,post):
+		from .thread import post_jump
+		f = self.field
+		db = get_db()
+
+		thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone()
+		threadpage = url_for('thread.view_thread',thread_id=thread['id'],_external=True)
+
+		forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone()
+		forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True)
+
+		username = post['author']
+		userpage = url_for('user.view_user',username=username,_external=True)
+
+		payload = {
+			"username":"apioforum",
+			"avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png",
+			"embeds":[
+				{
+					"title":"re: "+thread['title'],
+					"description":abridge(post['content']),
+					"url": post_jump(post['id'],external=True),
+					"color": 0x00ffff,
+					"fields":[
+						f('author',f"[{username}]({userpage})"),
+						f('thread',f"[{thread['title']}]({threadpage})"),
+						f('forum',f"[{forum['name']}]({forumpage})"),
+					],
+					"footer":{
+						"text":post['created'].isoformat(' '),
+					},
+				},
+			],
+		}
+		self.send(payload)
 
 
 @webhook_type("apionet")
 class ApionetWebhook(WebhookType):
-    MAXMSGLEN = 420
+	MAXMSGLEN = 420
 
-    # using 'url' as path of socket to send to.
-    def send(self,payload):
-        import socket
-        s = socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM)
-        s.sendto(payload.encode("utf-8"),self.url)
-        s.close()
+	# using 'url' as path of socket to send to.
+	def send(self,payload):
+		import socket
+		s = socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM)
+		s.sendto(payload.encode("utf-8"),self.url)
+		s.close()
 
-    def append_url(self,rest,url):
-        ub = url.encode("utf-8")
-        max_rlen = self.MAXMSGLEN - len(ub) - 1
-        
-        while len(rest.encode("utf-8")) > max_rlen:
-            # chop off characters until short enough
-            rest = rest[:-1]
+	def append_url(self,rest,url):
+		ub = url.encode("utf-8")
+		max_rlen = self.MAXMSGLEN - len(ub) - 1
+		
+		while len(rest.encode("utf-8")) > max_rlen:
+			# chop off characters until short enough
+			rest = rest[:-1]
 
-        return rest + " " + url
+		return rest + " " + url
 
-    def on_new_thread(self,thread):
-        # copy paste from above. the great refactor will fix this all
-        db = get_db()
-        forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone()
-        username = thread['creator'][:30]
-        post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone()
-        url = url_for('thread.view_thread',thread_id=thread['id'],_external=True)
+	def on_new_thread(self,thread):
+		# copy paste from above. the great refactor will fix this all
+		db = get_db()
+		forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone()
+		username = thread['creator'][:30]
+		post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone()
+		url = url_for('thread.view_thread',thread_id=thread['id'],_external=True)
 
 
-        p = ""
-        if forum['id'] != 1:
-            p = f" in {forum['name']}"
-        payload = f"new thread{p}: [{abridge(thread['title'],50)}] {username}: {abridge(post['content'],75)}"
-        self.send(self.append_url(payload,"("+url+")"))
+		p = ""
+		if forum['id'] != 1:
+			p = f" in {forum['name']}"
+		payload = f"new thread{p}: [{abridge(thread['title'],50)}] {username}: {abridge(post['content'],75)}"
+		self.send(self.append_url(payload,"("+url+")"))
 
-    def on_new_post(self,post):
-        from .thread import post_jump
-        db = get_db()
+	def on_new_post(self,post):
+		from .thread import post_jump
+		db = get_db()
 
-        thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone()
-        forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone()
-        username = post['author'][:30]
+		thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone()
+		forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone()
+		username = post['author'][:30]
 
-        url = post_jump(post['id'],external=True)
+		url = post_jump(post['id'],external=True)
 
 
-        payload = f"[{abridge(thread['title'],50)}] {username}: {abridge(post['content'],75)}"
-        self.send(self.append_url(payload,"("+url+")"))
+		payload = f"[{abridge(thread['title'],50)}] {username}: {abridge(post['content'],75)}"
+		self.send(self.append_url(payload,"("+url+")"))
 
 
 
-- 
cgit v1.2.3