A Python content management system designed for kirsle.net featuring a blog, comments and photo albums. https://rophako.kirsle.net/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

267 lines
8.8 KiB

  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals, absolute_import
  3. """Endpoints for the commenting subsystem."""
  4. from flask import Blueprint, g, request, redirect, url_for, flash
  5. import time
  6. import rophako.model.user as User
  7. import rophako.model.comment as Comment
  8. from rophako.utils import (template, pretty_time, sanitize_name, remote_addr)
  9. from rophako.plugin import load_plugin
  10. from rophako.settings import Config
  11. mod = Blueprint("comment", __name__, url_prefix="/comments")
  12. load_plugin("rophako.modules.emoticons")
  13. @mod.route("/")
  14. def index():
  15. return template("blog/index.html")
  16. @mod.route("/preview", methods=["POST"])
  17. def preview():
  18. # Get the form fields.
  19. form = get_comment_form(request.form)
  20. thread = sanitize_name(form["thread"])
  21. # Trap fields.
  22. trap1 = request.form.get("website", "x") != "http://"
  23. trap2 = request.form.get("email", "x") != ""
  24. if trap1 or trap2:
  25. flash("Wanna try that again?")
  26. return redirect(url_for("index"))
  27. # Validate things.
  28. if len(form["message"]) == 0:
  29. flash("You must provide a message with your comment.")
  30. return redirect(form["url"])
  31. # Gravatar?
  32. gravatar = Comment.gravatar(form["contact"])
  33. if g.info["session"]["login"]:
  34. form["name"] = g.info["session"]["name"]
  35. gravatar = "/".join([
  36. Config.photo.root_public,
  37. User.get_picture(uid=g.info["session"]["uid"]),
  38. ])
  39. # Are they submitting?
  40. if form["action"] == "submit":
  41. # Make sure they have a deletion token in their session.
  42. token = Comment.deletion_token()
  43. Comment.add_comment(
  44. thread=thread,
  45. uid=g.info["session"]["uid"],
  46. ip=remote_addr(),
  47. time=int(time.time()),
  48. image=gravatar,
  49. name=form["name"],
  50. subject=form["subject"],
  51. message=form["message"],
  52. url=form["url"],
  53. token=token,
  54. )
  55. # Are we subscribing to the thread?
  56. if form["subscribe"] == "true":
  57. email = form["contact"]
  58. if "@" in email:
  59. Comment.add_subscriber(thread, email)
  60. flash("You have been subscribed to future comments on this page.")
  61. flash("Your comment has been added!")
  62. return redirect(form["url"])
  63. # Gravatar.
  64. g.info["gravatar"] = gravatar
  65. g.info["preview"] = Comment.format_message(form["message"])
  66. g.info["pretty_time"] = pretty_time(Config.comment.time_format, time.time())
  67. g.info.update(form)
  68. return template("comment/preview.html")
  69. @mod.route("/delete/<thread>/<cid>")
  70. def delete(thread, cid):
  71. """Delete a comment."""
  72. if not Comment.is_editable(thread, cid):
  73. flash("Permission denied; maybe you need to log in?")
  74. return redirect(url_for("account.login"))
  75. url = request.args.get("url")
  76. Comment.delete_comment(thread, cid)
  77. flash("Comment deleted!")
  78. return redirect(url or url_for("index"))
  79. @mod.route("/quickdelete/<token>")
  80. def quick_delete(token):
  81. """Quick-delete a comment.
  82. This is for the site admins: when a comment is posted, the admins' version
  83. of the email contains a quick deletion link in case of spam. The ``token``
  84. here is in relation to that. It's a signed hash via ``itsdangerous`` using
  85. the site's secret key so that users can't forge their own tokens.
  86. """
  87. data = Comment.validate_quick_delete_token(token)
  88. if data is None:
  89. flash("Permission denied: token not valid.")
  90. return redirect(url_for("index"))
  91. url = request.args.get("url")
  92. Comment.delete_comment(data["t"], data["c"])
  93. flash("Comment has been quick-deleted!")
  94. return redirect(url or url_for("index"))
  95. @mod.route("/edit/<thread>/<cid>", methods=["GET", "POST"])
  96. def edit(thread, cid):
  97. """Edit an existing comment."""
  98. if not Comment.is_editable(thread, cid):
  99. flash("Permission denied; maybe you need to log in?")
  100. return redirect(url_for("account.login"))
  101. url = request.args.get("url")
  102. comment = Comment.get_comment(thread, cid)
  103. if not comment:
  104. flash("The comment wasn't found!")
  105. return redirect(url or url_for("index"))
  106. # Submitting?
  107. if request.method == "POST":
  108. action = request.form.get("action")
  109. message = request.form.get("message")
  110. url = request.form.get("url") # Preserve the URL!
  111. if len(message) == 0:
  112. flash("The comment must have a message!")
  113. return redirect(url_for(".edit", thread=thread, cid=cid, url=url))
  114. # Update the real comment data with the submitted message (for preview),
  115. # if they clicked Save it will then be saved back to disk.
  116. comment["message"] = message
  117. if action == "save":
  118. # Saving the changes!
  119. Comment.update_comment(thread, cid, comment)
  120. flash("Comment updated successfully!")
  121. return redirect(url or url_for("index"))
  122. # Render the Markdown.
  123. comment["formatted_message"] = Comment.format_message(comment["message"])
  124. g.info["thread"] = thread
  125. g.info["cid"] = cid
  126. g.info["comment"] = comment
  127. g.info["url"] = url or ""
  128. return template("comment/edit.html")
  129. @mod.route("/privacy")
  130. def privacy():
  131. """The privacy policy and global unsubscribe page."""
  132. return template("comment/privacy.html")
  133. @mod.route("/unsubscribe", methods=["GET", "POST"])
  134. def unsubscribe():
  135. """Unsubscribe an e-mail from a comment thread (or all threads)."""
  136. # This endpoint can be called with either method. For the unsubscribe links
  137. # inside the e-mails, it uses GET. For the global out-opt, it uses POST.
  138. thread, email = None, None
  139. if request.method == "POST":
  140. thread = request.form.get("thread", "")
  141. email = request.form.get("email", "")
  142. # Spam check.
  143. trap1 = request.form.get("url", "x") != "http://"
  144. trap2 = request.form.get("message", "x") != ""
  145. if trap1 or trap2:
  146. flash("Wanna try that again?")
  147. return redirect(url_for("index"))
  148. else:
  149. thread = request.args.get("thread", "")
  150. email = request.args.get("who", "")
  151. # Input validation.
  152. if not thread:
  153. flash("Comment thread not found.")
  154. return redirect(url_for("index"))
  155. if not email:
  156. flash("E-mail address not provided.")
  157. return redirect(url_for("index"))
  158. # Do the unsubscribe. If thread is *, this means a global unsubscribe from
  159. # all threads.
  160. Comment.unsubscribe(thread, email)
  161. g.info["thread"] = thread
  162. g.info["email"] = email
  163. return template("comment/unsubscribed.html")
  164. def partial_index(thread, subject, header=True, addable=True):
  165. """Partial template for including the index view of a comment thread.
  166. Parameters:
  167. thread (str): the unique name for the comment thread.
  168. subject (str): subject name for the comment thread.
  169. header (bool): show the 'Comments' H1 header.
  170. addable (bool): can new comments be added to the thread?
  171. """
  172. # Get all the comments on this thread.
  173. comments = Comment.get_comments(thread)
  174. # Sort the comments by most recent on bottom.
  175. sorted_cids = [ x for x in sorted(comments, key=lambda y: comments[y]["time"]) ]
  176. sorted_comments = []
  177. for cid in sorted_cids:
  178. comment = comments[cid]
  179. comment["id"] = cid
  180. # Was the commenter logged in?
  181. if comment["uid"] > 0:
  182. user = User.get_user(uid=comment["uid"])
  183. avatar = User.get_picture(uid=comment["uid"])
  184. comment["name"] = user["name"]
  185. comment["username"] = user["username"]
  186. comment["image"] = avatar
  187. # Add the pretty time.
  188. comment["pretty_time"] = pretty_time(Config.comment.time_format, comment["time"])
  189. # Format the message for display.
  190. comment["formatted_message"] = Comment.format_message(comment["message"])
  191. # Was this comment posted by the current user viewing it?
  192. comment["editable"] = Comment.is_editable(thread, cid, comment)
  193. sorted_comments.append(comment)
  194. g.info["header"] = header
  195. g.info["thread"] = thread
  196. g.info["subject"] = subject
  197. g.info["commenting_disabled"] = not addable
  198. g.info["url"] = request.url
  199. g.info["comments"] = sorted_comments
  200. g.info["photo_url"] = Config.photo.root_public
  201. return template("comment/index.inc.html")
  202. def get_comment_form(form):
  203. return dict(
  204. action = request.form.get("action", ""),
  205. thread = request.form.get("thread", ""),
  206. url = request.form.get("url", ""),
  207. subject = request.form.get("subject", "[No Subject]"),
  208. name = request.form.get("name", ""),
  209. contact = request.form.get("contact", ""),
  210. message = request.form.get("message", ""),
  211. subscribe = request.form.get("subscribe", "false"),
  212. )