353 rader
10 KiB
Python
353 rader
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import unicode_literals, absolute_import
|
|
|
|
"""Commenting models."""
|
|
|
|
from flask import url_for, session
|
|
from itsdangerous import URLSafeSerializer
|
|
import time
|
|
import hashlib
|
|
import urllib
|
|
import random
|
|
import sys
|
|
import uuid
|
|
|
|
from rophako.settings import Config
|
|
import rophako.jsondb as JsonDB
|
|
import rophako.model.user as User
|
|
import rophako.model.emoticons as Emoticons
|
|
from rophako.utils import send_email, render_markdown
|
|
from rophako.log import logger
|
|
|
|
def deletion_token():
|
|
"""Retrieves the comment deletion token for the current user's session.
|
|
|
|
Deletion tokens are random strings saved with a comment's data that allows
|
|
its original commenter to delete or modify their comment on their own,
|
|
within a window of time configurable by the site owner
|
|
(in ``comment.edit_period``).
|
|
|
|
If the current session doesn't have a deletion token yet, this function
|
|
will generate and set one. Otherwise it returns the one set last time.
|
|
All comments posted by the same session would share the same deletion
|
|
token.
|
|
"""
|
|
if not "comment_token" in session:
|
|
session["comment_token"] = str(uuid.uuid4())
|
|
return session.get("comment_token")
|
|
|
|
|
|
def add_comment(thread, uid, name, subject, message, url, time, ip,
|
|
token=None, image=None):
|
|
"""Add a comment to a comment thread.
|
|
|
|
Parameters:
|
|
thread (str): the unique comment thread name.
|
|
uid (int): 0 for guest posts, otherwise the UID of the logged-in user.
|
|
name (str): the commenter's name (if a guest)
|
|
subject (str)
|
|
message (str)
|
|
url (str): the URL where the comment can be read (i.e. the blog post)
|
|
time (int): epoch time of the comment.
|
|
ip (str): the user's IP address.
|
|
token (str): the user's session's comment deletion token.
|
|
image (str): the URL to a Gravatar image, if any.
|
|
"""
|
|
|
|
# Get the comments for this thread.
|
|
comments = get_comments(thread)
|
|
|
|
# Make up a unique ID for the comment.
|
|
cid = random_hash()
|
|
while cid in comments:
|
|
cid = random_hash()
|
|
|
|
# Add the comment.
|
|
comments[cid] = dict(
|
|
uid=uid,
|
|
name=name or "Anonymous",
|
|
image=image or "",
|
|
message=message,
|
|
time=time or int(time.time()),
|
|
ip=ip,
|
|
token=token,
|
|
)
|
|
write_comments(thread, comments)
|
|
|
|
# Get info about the commenter.
|
|
if uid > 0:
|
|
user = User.get_user(uid=uid)
|
|
if user:
|
|
name = user["name"]
|
|
|
|
# Send the e-mail to the site admins.
|
|
send_email(
|
|
to=Config.site.notify_address,
|
|
subject="Comment Added: {}".format(subject),
|
|
message="""{name} has left a comment on: {subject}
|
|
|
|
{message}
|
|
|
|
-----
|
|
|
|
To view this comment, please go to <{url}>
|
|
|
|
Was this comment spam? [Delete it]({deletion_link}).""".format(
|
|
name=name,
|
|
subject=subject,
|
|
message=message,
|
|
url=url,
|
|
deletion_link=url_for("comment.quick_delete",
|
|
token=make_quick_delete_token(thread, cid),
|
|
url=url,
|
|
_external=True,
|
|
)
|
|
),
|
|
)
|
|
|
|
# Notify any subscribers.
|
|
subs = get_subscribers(thread)
|
|
for sub in subs.keys():
|
|
# Make the unsubscribe link.
|
|
unsub = url_for("comment.unsubscribe", thread=thread, who=sub, _external=True)
|
|
|
|
send_email(
|
|
to=sub,
|
|
subject="New Comment: {}".format(subject),
|
|
message="""Hello,
|
|
|
|
{name} has left a comment on: {subject}
|
|
|
|
{message}
|
|
|
|
-----
|
|
|
|
To view this comment, please go to <{url}>""".format(
|
|
thread=thread,
|
|
name=name,
|
|
subject=subject,
|
|
message=message,
|
|
url=url,
|
|
unsub=unsub,
|
|
),
|
|
footer="You received this e-mail because you subscribed to the "
|
|
"comment thread that this comment was added to. You may "
|
|
"[**unsubscribe**]({unsub}) if you like.".format(
|
|
unsub=unsub,
|
|
),
|
|
)
|
|
|
|
|
|
def get_comment(thread, cid):
|
|
"""Look up a specific comment."""
|
|
comments = get_comments(thread)
|
|
return comments.get(cid, None)
|
|
|
|
|
|
def update_comment(thread, cid, data):
|
|
"""Update the data for a comment."""
|
|
comments = get_comments(thread)
|
|
if cid in comments:
|
|
comments[cid].update(data)
|
|
write_comments(thread, comments)
|
|
|
|
|
|
def delete_comment(thread, cid):
|
|
"""Delete a comment from a thread."""
|
|
comments = get_comments(thread)
|
|
if cid in comments:
|
|
del comments[cid]
|
|
write_comments(thread, comments)
|
|
|
|
|
|
def make_quick_delete_token(thread, cid):
|
|
"""Generate a unique tamper-proof token for quickly deleting comments.
|
|
|
|
This allows for an instant 'delete' link to be included in the notification
|
|
e-mail sent to the site admins, to delete obviously spammy comments
|
|
quickly.
|
|
|
|
It uses ``itsdangerous`` to create a unique token signed by the site's
|
|
secret key so that users can't forge their own tokens.
|
|
|
|
Parameters:
|
|
thread (str): comment thread name.
|
|
cid (str): unique comment ID.
|
|
|
|
Returns:
|
|
str
|
|
"""
|
|
s = URLSafeSerializer(Config.security.secret_key)
|
|
return s.dumps(dict(
|
|
t=thread,
|
|
c=cid,
|
|
))
|
|
|
|
|
|
def validate_quick_delete_token(token):
|
|
"""Validate and decode a quick delete token.
|
|
|
|
If the token is valid, returns a dict of the thread name and comment ID,
|
|
as keys ``t`` and ``c`` respectively.
|
|
|
|
If not valid, returns ``None``.
|
|
"""
|
|
s = URLSafeSerializer(Config.security.secret_key)
|
|
try:
|
|
return s.loads(token)
|
|
except:
|
|
logger.exception("Failed to validate quick-delete token {}".format(token))
|
|
return None
|
|
|
|
|
|
def is_editable(thread, cid, comment=None):
|
|
"""Determine if the comment is editable by the end user.
|
|
|
|
A comment is editable to its own author (even guests) for a window defined
|
|
by the site owner. In this event, the user's session has their
|
|
'comment deletion token' that matches the comment's saved token, and the
|
|
comment was posted recently.
|
|
|
|
Site admins (any logged-in user) can always edit all comments.
|
|
|
|
Parameters:
|
|
thread (str): the unique comment thread name.
|
|
cid (str): the comment ID.
|
|
comment (dict): if you already have the comment object, you can provide
|
|
it here and save an extra DB lookup.
|
|
|
|
Returns:
|
|
bool: True if the user is logged in *OR* has a valid deletion token and
|
|
the comment is relatively new. Otherwise returns False.
|
|
"""
|
|
# Logged in users can always do it.
|
|
if session["login"]:
|
|
return True
|
|
|
|
# Get the comment, or bail if not found.
|
|
if comment is None:
|
|
comment = get_comment(thread, cid)
|
|
if not comment:
|
|
return False
|
|
|
|
# Make sure the comment's token matches the user's, or bail.
|
|
if comment.get("token", "x") != deletion_token():
|
|
return False
|
|
|
|
# And finally, make sure the comment is new enough.
|
|
return time.time() - comment["time"] < 60*60*Config.comment.edit_period
|
|
|
|
|
|
def count_comments(thread):
|
|
"""Count the comments on a thread."""
|
|
comments = get_comments(thread)
|
|
return len(comments.keys())
|
|
|
|
|
|
def add_subscriber(thread, email):
|
|
"""Add a subscriber to a thread."""
|
|
if not "@" in email:
|
|
return
|
|
|
|
# Sanity check: only subscribe to threads that exist.
|
|
if not JsonDB.exists("comments/threads/{}".format(thread)):
|
|
return
|
|
|
|
logger.info("Subscribe e-mail {} to thread {}".format(email, thread))
|
|
subs = get_subscribers(thread)
|
|
subs[email] = int(time.time())
|
|
write_subscribers(thread, subs)
|
|
|
|
|
|
def unsubscribe(thread, email):
|
|
"""Unsubscribe an e-mail address from a thread.
|
|
|
|
If `thread` is `*`, the e-mail is unsubscribed from all threads."""
|
|
|
|
# Which threads to unsubscribe from?
|
|
threads = []
|
|
if thread == "*":
|
|
threads = JsonDB.list_docs("comments/subscribers")
|
|
else:
|
|
threads = [thread]
|
|
|
|
# Remove them as a subscriber.
|
|
for thread in threads:
|
|
if JsonDB.exists("comments/subscribers/{}".format(thread)):
|
|
logger.info("Unsubscribe e-mail address {} from comment thread {}".format(email, thread))
|
|
db = get_subscribers(thread)
|
|
del db[email]
|
|
write_subscribers(thread, db)
|
|
|
|
|
|
def format_message(message):
|
|
"""HTML sanitize the message and format it for display."""
|
|
|
|
# Comments use Markdown formatting, and HTML tags are escaped by default.
|
|
message = render_markdown(message)
|
|
|
|
# Process emoticons.
|
|
message = Emoticons.render(message)
|
|
return message
|
|
|
|
|
|
def get_comments(thread):
|
|
"""Get the comment thread."""
|
|
doc = "comments/threads/{}".format(thread)
|
|
if JsonDB.exists(doc):
|
|
return JsonDB.get(doc)
|
|
return {}
|
|
|
|
|
|
def write_comments(thread, comments):
|
|
"""Save the comments DB."""
|
|
if len(comments.keys()) == 0:
|
|
return JsonDB.delete("comments/threads/{}".format(thread))
|
|
return JsonDB.commit("comments/threads/{}".format(thread), comments)
|
|
|
|
|
|
def get_subscribers(thread):
|
|
"""Get the subscribers to a comment thread."""
|
|
doc = "comments/subscribers/{}".format(thread)
|
|
if JsonDB.exists(doc):
|
|
return JsonDB.get(doc)
|
|
return {}
|
|
|
|
|
|
def write_subscribers(thread, subs):
|
|
"""Save the subscribers to the DB."""
|
|
if len(subs.keys()) == 0:
|
|
return JsonDB.delete("comments/subscribers/{}".format(thread))
|
|
return JsonDB.commit("comments/subscribers/{}".format(thread), subs)
|
|
|
|
|
|
def random_hash():
|
|
"""Get a short random hash to use as the ID for a comment."""
|
|
md5 = hashlib.md5()
|
|
md5.update(str(random.randint(0, 1000000)).encode("utf-8"))
|
|
return md5.hexdigest()
|
|
|
|
|
|
def gravatar(email):
|
|
"""Generate a Gravatar link for an email address."""
|
|
if "@" in email:
|
|
# Default avatar?
|
|
default = Config.comment.default_avatar
|
|
|
|
# Construct the URL.
|
|
params = {
|
|
"s": "96", # size
|
|
}
|
|
if default:
|
|
params["d"] = default
|
|
url = "//www.gravatar.com/avatar/" + hashlib.md5(email.lower().encode("utf-8")).hexdigest() + "?"
|
|
|
|
# URL encode the params, the Python 2 & Python 3 way.
|
|
if sys.version_info[0] < 3:
|
|
url += urllib.urlencode(params)
|
|
else:
|
|
url += urllib.parse.urlencode(params)
|
|
|
|
return url
|
|
return ""
|