remotes/1693045480750635534/spooky-22
Aevann1 2021-10-08 04:14:54 +02:00
parent f4b6bb72c5
commit e69dbd0ed1
21 changed files with 1 additions and 129 deletions

View File

@ -98,8 +98,7 @@ def before_request():
g.timestamp = int(time.time())
#do not access session for static files
if not request.path.startswith("/assets"):
if not request.path.startswith("/assets") and not request.path.startswith("/images") and not request.path.startswith("/hostedimages"):
session.permanent = True
if not session.get("session_id"): session["session_id"] = secrets.token_hex(16)

View File

@ -102,9 +102,7 @@ class Comment(Base):
now = time.gmtime()
ctd = time.gmtime(self.created_utc)
# compute number of months
months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year)
# remove a month count if current day of month < creation day of month
if now.tm_mday < ctd.tm_mday:
months -= 1
@ -234,7 +232,6 @@ class Comment(Base):
'score': self.score,
'upvotes': self.upvotes,
'downvotes': self.downvotes,
#'award_count': self.award_count,
'is_bot': self.is_bot,
'flags': flags,
}

View File

@ -53,9 +53,7 @@ class ModAction(Base):
now = time.gmtime()
ctd = time.gmtime(self.created_utc)
# compute number of months
months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year)
# remove a month count if current day of month < creation day of month
if now.tm_mday < ctd.tm_mday:
months -= 1

View File

@ -107,9 +107,7 @@ class Submission(Base):
now = time.gmtime()
ctd = time.gmtime(self.created_utc)
# compute number of months
months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year)
# remove a month count if current day of month < creation day of month
if now.tm_mday < ctd.tm_mday:
months -= 1
@ -266,7 +264,6 @@ class Submission(Base):
'downvotes': self.downvotes,
'stickied': self.stickied,
'distinguish_level': self.distinguish_level,
#'award_count': self.award_count,
'voted': self.voted if hasattr(self, 'voted') else 0,
'flags': flags,
}

View File

@ -613,9 +613,7 @@ class ViewerRelationship(Base):
now = time.gmtime()
ctd = time.gmtime(self.created_utc)
# compute number of months
months = now.tm_mon - ctd.tm_mon + 12 * (now.tm_year - ctd.tm_year)
# remove a month count if current day of month < creation day of month
if now.tm_mday < ctd.tm_mday:
months -= 1

View File

@ -9,7 +9,6 @@ from .const import *
def send_notification(vid, user, text):
# for when working outside request context
if isinstance(user, int):
uid = user
else:

View File

@ -20,7 +20,6 @@ def filter_comment_html(html_text):
domain = urlparse(href).netloc
# parse domain into all possible subdomains
parts = domain.split(".")
for i in range(len(parts)):
new_domain = parts[i]
@ -29,7 +28,6 @@ def filter_comment_html(html_text):
domain_list.add(new_domain)
# search db for domain rules that prohibit commenting
bans = [x for x in g.db.query(BannedDomain).options(lazyload('*')).filter(BannedDomain.domain.in_(list(domain_list))).all()]
if bans:

View File

@ -250,7 +250,6 @@ def get_comments(cids, v=None, load_parent=False):
def get_domain(s):
# parse domain into all possible subdomains
parts = s.split(".")
domain_list = set([])
for i in range(len(parts)):
@ -267,8 +266,6 @@ def get_domain(s):
if not doms:
return None
# return the most specific domain - the one with the longest domain
# property
doms = sorted(doms, key=lambda x: len(x.domain), reverse=True)
return doms[0]

View File

@ -148,7 +148,6 @@ def sanitize(sanitized, noimages=False):
tag.wrap(link)
#disguised link preventer
for tag in soup.find_all("a"):
tag["target"] = "_blank"
@ -160,16 +159,13 @@ def sanitize(sanitized, noimages=False):
except:
tag.string = ""
#clean up tags in code
for tag in soup.find_all("code"):
tag.contents=[x.string for x in tag.contents if x.string]
#whatever else happens with images, there are only two sets of classes allowed
for tag in soup.find_all("img"):
if 'profile-pic-20' not in tag.attrs.get("class",""):
tag.attrs['class']="in-comment-image rounded-sm my-2"
#table format
for tag in soup.find_all("table"):
tag.attrs['class']="table table-striped"

View File

@ -107,7 +107,6 @@ def auth_desired(f):
def auth_required(f):
# decorator for any view that requires login (ex. settings)
def wrapper(*args, **kwargs):
@ -120,7 +119,6 @@ def auth_required(f):
g.v = v
# an ugly hack to make api work
resp = make_response(f(*args, v=v, **kwargs))
return resp
@ -129,7 +127,6 @@ def auth_required(f):
def is_not_banned(f):
# decorator that enforces lack of ban
def wrapper(*args, **kwargs):

View File

@ -658,7 +658,6 @@ def admin_image_ban(v):
i=request.files['file']
#make phash
tempname = f"admin_image_ban_{v.username}_{int(time.time())}"
i.save(tempname)
@ -668,20 +667,17 @@ def admin_image_ban(v):
value = int(str(h), 16)
bindigits = []
# Seed digit: 2**0
digit = (value % 2)
value //= 2
bindigits.append(digit)
while value > 0:
# Next power of 2**n
digit = (value % 2)
value //= 2
bindigits.append(digit)
h = ''.join([str(d) for d in bindigits])
#check db for existing
badpic = g.db.query(BadPic).options(lazyload('*')).filter_by(
phash=h
).first()
@ -874,7 +870,6 @@ def ban_user(user_id, v):
if user.admin_level >= v.admin_level: abort(403)
# check for number of days for suspension
if 'form' in request.values:
days = float(request.values.get("days")) if request.values.get('days') else 0
reason = sanitize(request.values.get("reason", ""))

View File

@ -143,7 +143,6 @@ def api_comment(v):
level = parent.level + 1
else: abort(400)
#process and sanitize
body = request.values.get("body", "")[:10000]
body = body.strip()
@ -162,7 +161,6 @@ def api_comment(v):
body_md = CustomRenderer().render(mistletoe.Document(body_md))
body_html = sanitize(body_md)
# Run safety filter
bans = filter_comment_html(body_html)
if bans:
@ -171,14 +169,12 @@ def api_comment(v):
if ban.reason:
reason += f" {ban.reason}"
#auto ban for digitally malicious content
if any([x.reason==4 for x in bans]):
v.ban(days=30, reason="Digitally malicious content")
if any([x.reason==7 for x in bans]):
v.ban( reason="Sexualizing minors")
return {"error": reason}, 401
# check existing
existing = g.db.query(Comment).options(lazyload('*')).filter(Comment.author_id == v.id,
Comment.deleted_utc == 0,
Comment.parent_comment_id == parent_comment_id,
@ -191,10 +187,8 @@ def api_comment(v):
if parent.author.any_block_exists(v) and not v.admin_level>=3:
return {"error": "You can't reply to users who have blocked you, or users you have blocked."}, 403
# get bot status
is_bot = request.headers.get("X-User-Type","")=="Bot"
# check spam - this should hopefully be faster
if not is_bot:
now = int(time.time())
cutoff = now - 60 * 60 * 24
@ -503,7 +497,6 @@ def api_comment(v):
if not v.shadowbanned:
# queue up notification for parent author
notify_users = set()
for x in g.db.query(Subscription.user_id).options(lazyload('*')).filter_by(submission_id=c.parent_submission).all():
@ -547,7 +540,6 @@ def api_comment(v):
# create auto upvote
vote = CommentVote(user_id=v.id,
comment_id=c.id,
vote_type=1
@ -599,7 +591,6 @@ def edit_comment(cid, v):
ban = bans[0]
reason = f"Remove the {ban.domain} link from your comment and try again."
#auto ban for digitally malicious content
if any([x.reason==4 for x in bans]):
v.ban(days=30, reason="Digitally malicious content is not allowed.")
return {"error":"Digitally malicious content is not allowed."}
@ -614,7 +605,6 @@ def edit_comment(cid, v):
body=body,
v=v
)
# check spam - this should hopefully be faster
now = int(time.time())
cutoff = now - 60 * 60 * 24
@ -741,7 +731,6 @@ def edit_comment(cid, v):
g.db.flush()
# queue up notifications for username mentions
notify_users = set()
soup = BeautifulSoup(body_html, features="html.parser")
mentions = soup.find_all("a", href=re.compile("^/@(\w+)"))

View File

@ -36,7 +36,6 @@ def join_discord(v):
def discord_redirect(v):
#validate state
now=int(time.time())
state=request.values.get('state','').split('.')
@ -50,7 +49,6 @@ def discord_redirect(v):
if not validate_hash(f"{timestamp}+{v.id}+discord", state):
abort(400)
#get discord token
code = request.values.get("code","")
if not code:
abort(400)
@ -79,7 +77,6 @@ def discord_redirect(v):
abort(403)
#get user ID
url="https://discord.com/api/users/@me"
headers={
'Authorization': f"Bearer {token}"
@ -90,13 +87,11 @@ def discord_redirect(v):
#add user to discord
headers={
'Authorization': f"Bot {BOT_TOKEN}",
'Content-Type': "application/json"
}
#remove existing user if applicable
if v.discord_id and v.discord_id != x['id']:
url=f"https://discord.com/api/guilds/{SERVER_ID}/members/{v.discord_id}"
requests.delete(url, headers=headers)

View File

@ -121,7 +121,6 @@ def front_all(v):
try: page = int(request.values.get("page") or 1)
except: abort(400)
# prevent invalid paging
page = max(page, 1)
if v:
@ -243,11 +242,9 @@ def changelog(v):
v=v,
)
# check existence of next page
next_exists = (len(ids) > 25)
ids = ids[:25]
# check if ids exist
posts = get_posts(ids, v=v)
if request.headers.get("Authorization"): return {"data": [x.json for x in posts], "next_exists": next_exists}

View File

@ -22,12 +22,10 @@ def login_get(v):
def check_for_alts(current_id):
# account history
past_accs = set(session.get("history", []))
past_accs.add(current_id)
session["history"] = list(past_accs)
# record alts
for past_id in session["history"]:
if past_id == current_id:
@ -95,7 +93,6 @@ def login_post():
time.sleep(random.uniform(0, 2))
return render_template("login.html", failed=True)
# test password
if request.values.get("password"):
@ -141,7 +138,6 @@ def login_post():
account.unban_utc = 0
g.db.add(account)
# set session and user id
session["user_id"] = account.id
session["session_id"] = token_hex(16)
session["login_nonce"] = account.login_nonce
@ -149,7 +145,6 @@ def login_post():
check_for_alts(account.id)
# check for previous page
redir = request.values.get("redirect", "/").replace("/logged_out", "")
@ -189,7 +184,6 @@ def sign_up_get(v):
agent = request.headers.get("User-Agent", None)
if not agent: abort(403)
# check for referral in link
ref = request.values.get("ref", None)
if ref:
ref_user = g.db.query(User).options(lazyload('*')).filter(User.username.ilike(ref)).first()
@ -200,14 +194,12 @@ def sign_up_get(v):
if ref_user and (ref_user.id in session.get("history", [])):
return render_template("sign_up_failed_ref.html")
# Make a unique form key valid for one account creation
now = int(time.time())
token = token_hex(16)
session["signup_token"] = token
formkey_hashstr = str(now) + token + agent
# formkey is a hash of session token, timestamp, and IP address
formkey = hmac.new(key=bytes(environ.get("MASTER_KEY"), "utf-16"),
msg=bytes(formkey_hashstr, "utf-16"),
digestmod='md5'
@ -258,8 +250,6 @@ def sign_up_post(v):
username = request.values.get("username").strip()
# define function that takes an error message and generates a new signup
# form
def new_signup(error):
args = {"error": error}
@ -337,7 +327,6 @@ def sign_up_post(v):
if id_1 == 0 and users_count < 6: admin_level=6
else: admin_level=0
# make new user
new_user = User(
username=username,
original_username = username,
@ -354,14 +343,11 @@ def sign_up_post(v):
g.db.add(new_user)
g.db.flush()
# check alts
check_for_alts(new_user.id)
# send welcome/verify email
if email: send_verification_email(new_user)
# send welcome message
if "rdrama" in request.host: send_notification(NOTIFICATIONS_ACCOUNT, new_user, "Dude bussy lmao")
session["user_id"] = new_user.id
@ -402,7 +388,6 @@ def post_forgot():
User.email.ilike(email)).first()
if user:
# generate url
now = int(time.time())
token = generate_hash(f"{user.id}+{now}+forgot+{user.login_nonce}")
url = f"https://{app.config['SERVER_NAME']}/reset?id={user.id}&time={now}&token={token}"
@ -533,7 +518,6 @@ def request_2fa_disable():
title="Removal request received",
message="If username, password, and email match, we will send you an email.")
#compute token
valid=int(time.time())
token=generate_hash(f"{user.id}+{user.username}+disable2fa+{valid}+{user.mfa_secret}+{user.login_nonce}")
@ -569,7 +553,6 @@ def reset_2fa():
if not validate_hash(f"{user.id}+{user.username}+disable2fa+{t}+{user.mfa_secret}+{user.login_nonce}", token):
abort(403)
#validation successful, remove 2fa
user.mfa_secret=None
g.db.add(user)

View File

@ -211,7 +211,6 @@ def edit_post(pid, v):
body_md = CustomRenderer().render(mistletoe.Document(body))
body_html = sanitize(body_md)
# Run safety filter
bans = filter_comment_html(body_html)
if bans:
ban = bans[0]
@ -219,7 +218,6 @@ def edit_post(pid, v):
if ban.reason:
reason += f" {ban.reason}"
#auto ban for digitally malicious content
if any([x.reason==4 for x in bans]):
v.ban(days=30, reason="Digitally malicious content is not allowed.")
abort(403)
@ -347,7 +345,6 @@ def filter_title(title):
title = title.replace("\r", "")
title = title.replace("\t", "")
# sanitize title
title = bleach.clean(title, tags=[])
for i in re.finditer('(?<!"):([^ ]{1,30}?):', title):
@ -370,7 +367,6 @@ def thumbnail_thread(pid):
def expand_url(post_url, fragment_url):
# convert src into full url
if fragment_url.startswith("https://"):
return fragment_url
elif fragment_url.startswith("http://"):
@ -393,7 +389,6 @@ def thumbnail_thread(pid):
fetch_url=post.url
#mimic chrome browser agent
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36"}
try:
@ -407,17 +402,12 @@ def thumbnail_thread(pid):
return
#if content is image, stick with that. Otherwise, parse html.
if x.headers.get("Content-Type","").startswith("text/html"):
#parse html, find image, load image
soup=BeautifulSoup(x.content, 'html.parser')
#parse html
#create list of urls to check
thumb_candidate_urls=[]
#iterate through desired meta tags
meta_tags = [
"ruqqus:thumbnail",
"twitter:image",
@ -446,12 +436,10 @@ def thumbnail_thread(pid):
if tag:
thumb_candidate_urls.append(expand_url(post.url, tag['content']))
#parse html doc for <img> elements
for tag in soup.find_all("img", attrs={'src':True}):
thumb_candidate_urls.append(expand_url(post.url, tag['src']))
#now we have a list of candidate urls to try
for url in thumb_candidate_urls:
try:
@ -475,14 +463,12 @@ def thumbnail_thread(pid):
break
else:
#getting here means we are out of candidate urls (or there never were any)
db.close()
return
elif x.headers.get("Content-Type","").startswith("image/"):
#image is originally loaded fetch_url
image_req=x
image = PILimage.open(BytesIO(x.content))
@ -569,7 +555,6 @@ def submit_post(v):
url = ""
body = request.values.get("body", "")
# check for duplicate
dup = g.db.query(Submission).options(lazyload('*')).filter(
Submission.author_id == v.id,
@ -583,13 +568,11 @@ def submit_post(v):
return redirect(dup.permalink)
# check for domain specific rules
parsed_url = urlparse(url)
domain = parsed_url.netloc
# check ban status
domain_obj = get_domain(domain)
if domain_obj:
if domain_obj.reason==4:
@ -620,7 +603,6 @@ def submit_post(v):
else: embed = None
# similarity check
now = int(time.time())
cutoff = now - 60 * 60 * 24
@ -628,34 +610,18 @@ def submit_post(v):
similar_posts = g.db.query(Submission).options(
lazyload('*')
).filter(
#or_(
# and_(
Submission.author_id == v.id,
Submission.title.op('<->')(title) < app.config["SPAM_SIMILARITY_THRESHOLD"],
Submission.created_utc > cutoff
# ),
# and_(
# Submission.title.op('<->')(title) < app.config["SPAM_SIMILARITY_THRESHOLD"]/2,
# Submission.created_utc > cutoff
# )
#)
).all()
if url:
similar_urls = g.db.query(Submission).options(
lazyload('*')
).filter(
#or_(
# and_(
Submission.author_id == v.id,
Submission.url.op('<->')(url) < app.config["SPAM_URL_SIMILARITY_THRESHOLD"],
Submission.created_utc > cutoff
# ),
# and_(
# Submission.url.op('<->')(url) < app.config["SPAM_URL_SIMILARITY_THRESHOLD"]/2,
# Submission.created_utc > cutoff
# )
#)
).all()
else:
similar_urls = []
@ -692,7 +658,6 @@ def submit_post(v):
g.db.add(ma)
return redirect("/notifications")
# catch too-long body
if len(str(body)) > 10000:
if request.headers.get("Authorization"): return {"error":"10000 character limit for text body."}, 400
@ -703,7 +668,6 @@ def submit_post(v):
if request.headers.get("Authorization"): return {"error":"2048 character limit for URLs."}, 400
else: return render_template("submit.html", v=v, error="2048 character limit for URLs.", title=title, url=url,body=request.values.get("body", "")), 400
# render text
for i in re.finditer('^(https:\/\/.*\.(png|jpg|jpeg|gif|webp|PNG|JPG|JPEG|GIF|WEBP|9999))', body, re.MULTILINE):
if "wikipedia" not in i.group(1): body = body.replace(i.group(1), f'![]({i.group(1)})')
body = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', body)
@ -720,7 +684,6 @@ def submit_post(v):
if len(body_html) > 20000: abort(400)
# Run safety filter
bans = filter_comment_html(body_html)
if bans:
ban = bans[0]
@ -728,7 +691,6 @@ def submit_post(v):
if ban.reason:
reason += f" {ban.reason}"
#auto ban for digitally malicious content
if any([x.reason==4 for x in bans]):
v.ban(days=30, reason="Digitally malicious content is not allowed.")
abort(403)
@ -736,7 +698,6 @@ def submit_post(v):
if request.headers.get("Authorization"): return {"error": reason}, 403
else: return render_template("submit.html", v=v, error=reason, title=title, url=url, body=request.values.get("body", "")), 403
# check for embeddable video
domain = parsed_url.netloc
if v.paid_dues: club = bool(request.values.get("club",""))

View File

@ -14,7 +14,6 @@ valid_params=[
def searchparse(text):
#takes test in filter:term format and returns data
criteria = {x[0]:x[1] for x in query_regex.findall(text)}

View File

@ -107,7 +107,6 @@ def settings_profile_post(v):
if "wikipedia" not in i.group(1): bio = bio.replace(i.group(1), f'![]({i.group(1)})')
bio = re.sub('([^\n])\n([^\n])', r'\1\n\n\2', bio)
# check for uploaded image
if request.files.get('file') and request.headers.get("cf-ipcountry") != "T1":
file = request.files['file']
@ -123,7 +122,6 @@ def settings_profile_post(v):
bio_html = CustomRenderer().render(mistletoe.Document(bio))
bio_html = sanitize(bio_html)
# Run safety filter
bans = filter_comment_html(bio_html)
if len(bio_html) > 10000:
@ -137,7 +135,6 @@ def settings_profile_post(v):
if ban.reason:
reason += f" {ban.reason}"
#auto ban for digitally malicious content
if any([x.reason==4 for x in bans]):
v.ban(days=30, reason="Digitally malicious content is not allowed.")
return {"error": reason}, 401
@ -416,7 +413,6 @@ def settings_security_post(v):
if new_email == v.email:
return redirect("/settings/security?error=That email is already yours!")
# check to see if email is in use
existing = g.db.query(User).options(lazyload('*')).filter(User.id != v.id,
func.lower(User.email) == new_email.lower()).first()
if existing:
@ -492,10 +488,8 @@ def settings_log_out_others(v):
if not v.verifyPass(submitted_password): return render_template("settings_security.html", v=v, error="Incorrect Password"), 401
# increment account's nonce
v.login_nonce += 1
# update cookie accordingly
session["login_nonce"] = v.login_nonce
g.db.add(v)
@ -865,7 +859,6 @@ def settings_title_change(v):
new_name=request.values.get("title").strip()[:100].replace("𒐪","")
#make sure name is different
if new_name==v.customtitle:
return render_template("settings_profile.html",
v=v,

View File

@ -100,7 +100,6 @@ def cached_chart():
comment_stats = [g.db.query(Comment.id).options(lazyload('*')).filter(Comment.created_utc < day_cutoffs[i], Comment.created_utc > day_cutoffs[i + 1],Comment.is_banned == False, Comment.author_id != 1).count() for i in range(len(day_cutoffs) - 1)][2:][::-1]
# create multiple charts
signup_chart = plt.subplot2grid((20, 4), (0, 0), rowspan=5, colspan=4)
posts_chart = plt.subplot2grid((20, 4), (7, 0), rowspan=5, colspan=4)
comments_chart = plt.subplot2grid((20, 4), (14, 0), rowspan=5, colspan=4)

View File

@ -395,13 +395,10 @@ def u_username(username, v=None):
if v and request.path.startswith('/logged_out'): v = None
# username is unique so at most this returns one result. Otherwise 404
# case insensitive search
u = get_user(username, v=v)
# check for wrong cases
if username != u.username:
return redirect(request.path.replace(username, u.username))
@ -410,7 +407,6 @@ def u_username(username, v=None):
if request.headers.get("Authorization"): return {"error": f"That username is reserved for: {u.reserved}"}
else: return render_template("userpage_reserved.html", u=u, v=v)
# viewers
if v and u.id != v.id:
view = g.db.query(ViewerRelationship).options(lazyload('*')).filter(
and_(
@ -457,7 +453,6 @@ def u_username(username, v=None):
ids = u.userpagelisting(v=v, page=page, sort=sort, t=t)
# we got 26 items just to see if a next page exists
next_exists = (len(ids) > 25)
ids = ids[:25]
@ -508,13 +503,10 @@ def u_username_comments(username, v=None):
if v and request.path.startswith('/logged_out'): v = None
# username is unique so at most this returns one result. Otherwise 404
# case insensitive search
user = get_user(username, v=v)
# check for wrong cases
if username != user.username: return redirect(f'{user.url}/comments')
@ -589,7 +581,6 @@ def u_username_comments(username, v=None):
comments = comments.offset(25 * (page - 1)).limit(26).all()
ids = [x.id for x in comments]
# we got 26 items just to see if a next page exists
next_exists = (len(ids) > 25)
ids = ids[:25]
@ -624,7 +615,6 @@ def follow_user(username, v):
if target.id==v.id: return {"error": "You can't follow yourself!"}, 400
# check for existing follow
if g.db.query(Follow).options(lazyload('*')).filter_by(user_id=v.id, target_id=target.id).first(): return {"message": "User followed!"}
new_follow = Follow(user_id=v.id, target_id=target.id)
@ -650,7 +640,6 @@ def unfollow_user(username, v):
if target.id == 995: abort(403)
# check for existing follow
follow = g.db.query(Follow).options(lazyload('*')).filter_by(user_id=v.id, target_id=target.id).first()
if not follow: return {"message": "User unfollowed!"}
@ -674,7 +663,6 @@ def unfollow_user(username, v):
def remove_follow(username, v):
target = get_user(username)
# check for existing follow
follow = g.db.query(Follow).options(lazyload('*')).filter_by(user_id=target.id, target_id=v.id).first()
if not follow: return {"message": "Follower removed!"}

View File

@ -62,14 +62,12 @@ def api_vote_post(post_id, new, v):
if new not in ["-1", "0", "1"]: abort(400)
# disallow bots
if request.headers.get("X-User-Type","") == "Bot": abort(403)
new = int(new)
post = get_post(post_id)
# check for existing vote
existing = g.db.query(Vote).options(lazyload('*')).filter_by(user_id=v.id, submission_id=post.id).first()
if existing and existing.vote_type == new: return "", 204
@ -127,7 +125,6 @@ def api_vote_comment(comment_id, new, v):
comment = get_comment(comment_id)
# check for existing vote
existing = g.db.query(CommentVote).options(lazyload('*')).filter_by(user_id=v.id, comment_id=comment.id).first()
if existing and existing.vote_type == new: return "", 204