Browse Source

Add Comments and Emoticons modules

pull/2/head
Noah Petherbridge 8 years ago
parent
commit
4bb9b5d687
  1. 28
      config-sample.py
  2. 8
      rophako/__init__.py
  3. 10
      rophako/jsondb.py
  4. 234
      rophako/model/comment.py
  5. 81
      rophako/model/emoticons.py
  6. 18
      rophako/modules/blog.py
  7. 186
      rophako/modules/comment.py
  8. 30
      rophako/modules/emoticons.py
  9. 27
      rophako/utils.py
  10. 8
      rophako/www/blog/entry.inc.html
  11. 2
      rophako/www/blog/update.html
  12. 90
      rophako/www/comment/index.inc.html
  13. 32
      rophako/www/comment/preview.html
  14. 61
      rophako/www/comment/privacy.html
  15. 14
      rophako/www/comment/unsubscribed.html
  16. 30
      rophako/www/emoticons/index.html
  17. BIN
      rophako/www/static/smileys/tango/act-up.png
  18. BIN
      rophako/www/static/smileys/tango/airplane.png
  19. BIN
      rophako/www/static/smileys/tango/alien.png
  20. BIN
      rophako/www/static/smileys/tango/angel.png
  21. BIN
      rophako/www/static/smileys/tango/angry.png
  22. BIN
      rophako/www/static/smileys/tango/arrogant.png
  23. BIN
      rophako/www/static/smileys/tango/bad.png
  24. BIN
      rophako/www/static/smileys/tango/bashful.png
  25. BIN
      rophako/www/static/smileys/tango/beat-up.png
  26. BIN
      rophako/www/static/smileys/tango/beauty.png
  27. BIN
      rophako/www/static/smileys/tango/beer.png
  28. BIN
      rophako/www/static/smileys/tango/blowkiss.png
  29. BIN
      rophako/www/static/smileys/tango/bomb.png
  30. BIN
      rophako/www/static/smileys/tango/bowl.png
  31. BIN
      rophako/www/static/smileys/tango/boy.png
  32. BIN
      rophako/www/static/smileys/tango/brb.png
  33. BIN
      rophako/www/static/smileys/tango/bye.png
  34. BIN
      rophako/www/static/smileys/tango/cake.png
  35. BIN
      rophako/www/static/smileys/tango/call-me.png
  36. BIN
      rophako/www/static/smileys/tango/camera.png
  37. BIN
      rophako/www/static/smileys/tango/can.png
  38. BIN
      rophako/www/static/smileys/tango/car.png
  39. BIN
      rophako/www/static/smileys/tango/cat.png
  40. BIN
      rophako/www/static/smileys/tango/chicken.png
  41. BIN
      rophako/www/static/smileys/tango/clap.png
  42. BIN
      rophako/www/static/smileys/tango/clock.png
  43. BIN
      rophako/www/static/smileys/tango/cloudy.png
  44. BIN
      rophako/www/static/smileys/tango/clover.png
  45. BIN
      rophako/www/static/smileys/tango/clown.png
  46. BIN
      rophako/www/static/smileys/tango/coffee.png
  47. BIN
      rophako/www/static/smileys/tango/coins.png
  48. BIN
      rophako/www/static/smileys/tango/computer.png
  49. BIN
      rophako/www/static/smileys/tango/confused.png
  50. BIN
      rophako/www/static/smileys/tango/console.png
  51. BIN
      rophako/www/static/smileys/tango/cow.png
  52. BIN
      rophako/www/static/smileys/tango/cowboy.png
  53. BIN
      rophako/www/static/smileys/tango/crying.png
  54. BIN
      rophako/www/static/smileys/tango/curl-lip.png
  55. BIN
      rophako/www/static/smileys/tango/curse.png
  56. BIN
      rophako/www/static/smileys/tango/cute.png
  57. BIN
      rophako/www/static/smileys/tango/dance.png
  58. BIN
      rophako/www/static/smileys/tango/dazed.png
  59. BIN
      rophako/www/static/smileys/tango/desire.png
  60. BIN
      rophako/www/static/smileys/tango/devil.png
  61. BIN
      rophako/www/static/smileys/tango/disapointed.png
  62. BIN
      rophako/www/static/smileys/tango/disdain.png
  63. BIN
      rophako/www/static/smileys/tango/doctor.png
  64. BIN
      rophako/www/static/smileys/tango/dog.png
  65. BIN
      rophako/www/static/smileys/tango/doh.png
  66. BIN
      rophako/www/static/smileys/tango/dont-know.png
  67. BIN
      rophako/www/static/smileys/tango/drink.png
  68. BIN
      rophako/www/static/smileys/tango/drool.png
  69. BIN
      rophako/www/static/smileys/tango/eat.png
  70. BIN
      rophako/www/static/smileys/tango/embarrassed.png
  71. 328
      rophako/www/static/smileys/tango/emoticons.json
  72. 86
      rophako/www/static/smileys/tango/emoticons.txt
  73. BIN
      rophako/www/static/smileys/tango/excruciating.png
  74. BIN
      rophako/www/static/smileys/tango/eyeroll.png
  75. BIN
      rophako/www/static/smileys/tango/film.png
  76. BIN
      rophako/www/static/smileys/tango/fingers-crossed.png
  77. BIN
      rophako/www/static/smileys/tango/flag.png
  78. BIN
      rophako/www/static/smileys/tango/foot-in-mouth.png
  79. BIN
      rophako/www/static/smileys/tango/freaked-out.png
  80. BIN
      rophako/www/static/smileys/tango/ghost.png
  81. BIN
      rophako/www/static/smileys/tango/giggle.png
  82. BIN
      rophako/www/static/smileys/tango/girl.png
  83. BIN
      rophako/www/static/smileys/tango/glasses-cool.png
  84. BIN
      rophako/www/static/smileys/tango/glasses-nerdy.png
  85. BIN
      rophako/www/static/smileys/tango/go-away.png
  86. BIN
      rophako/www/static/smileys/tango/goat.png
  87. BIN
      rophako/www/static/smileys/tango/good.png
  88. BIN
      rophako/www/static/smileys/tango/hammer.png
  89. BIN
      rophako/www/static/smileys/tango/handcuffs.png
  90. BIN
      rophako/www/static/smileys/tango/handshake.png
  91. BIN
      rophako/www/static/smileys/tango/highfive.png
  92. BIN
      rophako/www/static/smileys/tango/hug-left.png
  93. BIN
      rophako/www/static/smileys/tango/hug-right.png
  94. BIN
      rophako/www/static/smileys/tango/hungry.png
  95. BIN
      rophako/www/static/smileys/tango/hypnotized.png
  96. BIN
      rophako/www/static/smileys/tango/in-love.png
  97. BIN
      rophako/www/static/smileys/tango/island.png
  98. BIN
      rophako/www/static/smileys/tango/jump.png
  99. BIN
      rophako/www/static/smileys/tango/kiss.png
  100. BIN
      rophako/www/static/smileys/tango/knife.png

28
config-sample.py

@ -10,6 +10,9 @@ DEBUG = True
# Unique name of your site, e.g. "kirsle.net"
SITE_NAME = "example.com"
# E-mail addresses for site notifications (i.e. new comments).
NOTIFY_ADDRESS = ["root@localhost"]
# Secret key used for session cookie signing. Make this long and hard to guess.
#
# Tips for creating a strong secret key:
@ -36,6 +39,19 @@ REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PREFIX = "rophako:"
# Mail settings
MAIL_METHOD = "smtp" # or "sendmail", not yet implemented
MAIL_SERVER = "localhost"
MAIL_PORT = 25
MAIL_SENDER = "Rophako CMS <no-reply@rophako.kirsle.net>"
# Emoticon theme used for blog posts and comments. Should exist at the URL
# "/static/smileys" from your document root, and have a file named
# "emoticons.json" inside. If you add a custom theme to your private site
# folder, then also change EMOTICON_ROOT_PRIVATE to look there instead.
EMOTICON_THEME = "tango"
EMOTICON_ROOT_PRIVATE = os.path.join(_basedir, "rophako", "www", "static", "smileys")
################################################################################
## Blog Settings ##
################################################################################
@ -63,4 +79,14 @@ PHOTO_TIME_FORMAT = BLOG_TIME_FORMAT
# Photo sizes.
PHOTO_WIDTH_LARGE = 800 # Max width of full size photos.
PHOTO_WIDTH_THUMB = 256 # Max square width of photo thumbnails.
PHOTO_WIDTH_AVATAR = 96 # Square width of photo avatars.
PHOTO_WIDTH_AVATAR = 96 # Square width of photo avatars.
################################################################################
## Comment Settings ##
################################################################################
COMMENT_TIME_FORMAT = "%A, %B %d %Y @ %I:%M %p"
# We use Gravatar for comments if the user provides an e-mail address. Specify
# the URL to a fallback image to use in case they don't have a gravatar.
COMMENT_DEFAULT_AVATAR = ""

8
rophako/__init__.py

@ -19,10 +19,14 @@ from rophako.modules.admin import mod as AdminModule
from rophako.modules.account import mod as AccountModule
from rophako.modules.blog import mod as BlogModule
from rophako.modules.photo import mod as PhotoModule
from rophako.modules.comment import mod as CommentModule
from rophako.modules.emoticons import mod as EmoticonsModule
app.register_blueprint(AdminModule)
app.register_blueprint(AccountModule)
app.register_blueprint(BlogModule)
app.register_blueprint(PhotoModule)
app.register_blueprint(CommentModule)
app.register_blueprint(EmoticonsModule)
# Custom Jinja handler to support custom- and default-template folders for
# rendering templates.
@ -34,6 +38,10 @@ app.jinja_loader = jinja2.ChoiceLoader([
app.jinja_env.globals["csrf_token"] = rophako.utils.generate_csrf_token
app.jinja_env.globals["include_page"] = rophako.utils.include
# Preload the emoticon data.
import rophako.model.emoticons as Emoticons
Emoticons.load_theme()
@app.before_request
def before_request():

10
rophako/jsondb.py

@ -117,6 +117,11 @@ def read_json(path):
if not os.path.isfile(path):
raise Exception("Can't read JSON file {}: file not found!".format(path))
# Don't allow any fishy looking paths.
if ".." in path:
logger.error("ERROR: JsonDB tried to read a path with two dots: {}".format(path))
raise Exception()
# Open and lock the file.
fh = codecs.open(path, 'r', 'utf-8')
flock(fh, LOCK_SH)
@ -138,6 +143,11 @@ def write_json(path, data):
"""Write a JSON document."""
path = str(path)
# Don't allow any fishy looking paths.
if ".." in path:
logger.error("ERROR: JsonDB tried to write a path with two dots: {}".format(path))
raise Exception()
logger.debug("JsonDB: WRITE > {}".format(path))
# Open and lock the file.

234
rophako/model/comment.py

@ -0,0 +1,234 @@
# -*- coding: utf-8 -*-
"""Commenting models."""
from flask import g, url_for
import time
import hashlib
import urllib
import random
import re
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
from rophako.log import logger
def add_comment(thread, uid, name, subject, message, url, time, ip, image=None):
"""Add a comment to a comment thread.
* uid is 0 if it's a guest post, otherwise the UID of the user.
* name is the commenter's name (if a guest)
* subject is for the e-mails that are sent out
* message is self explanatory.
* url is the URL where the comment can be read.
* time, epoch time of comment.
* ip is the IP address of the commenter.
* image is a Gravatar image URL etc.
"""
# 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,
)
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.NOTIFY_ADDRESS,
subject="New comment: {}".format(subject),
message="""{name} has left a comment on: {subject}
{message}
To view this comment, please go to {url}
=====================
This e-mail was automatically generated. Do not reply to it.""".format(
name=name,
subject=subject,
message=message,
url=url,
),
)
# 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,
You are currently subscribed to the comment thread '{thread}', and somebody has
just added a new comment!
{name} has left a comment on: {subject}
{message}
To view this comment, please go to {url}
=====================
This e-mail was automatically generated. Do not reply to it.
If you wish to unsubscribe from this comment thread, please visit the following
URL: {unsub}""".format(
thread=thread,
name=name,
subject=subject,
message=message,
url=url,
unsub=unsub,
)
)
def delete_comment(thread, cid):
"""Delete a comment from a thread."""
comments = get_comments(thread)
del comments[cid]
write_comments(thread, comments)
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."""
# We basically want to escape HTML symbols (like what Flask does for us
# automatically), but we want line breaks to translate to literal <br> tags.
message = re.sub(r'&', '&amp;', message)
message = re.sub(r'<', '&lt;', message)
message = re.sub(r'>', '&gt;', message)
message = re.sub(r'"', '&quot;', message)
message = re.sub(r"'", '&apos;', message)
message = re.sub(r'\n', '<br>', message)
message = re.sub(r'\r', '', message)
# Process emoticons.
message = Emoticons.render(message)
return message
def get_comments(thread):
"""Get the comment thread."""
doc = "comments/threads/{}".format(thread)
print doc
if JsonDB.exists(doc):
return JsonDB.get(doc)
print "NOT EXIST"
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)))
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 = "http://www.gravatar.com/avatar/" + hashlib.md5(email.lower()).hexdigest() + "?"
url += urllib.urlencode(params)
return url
return ""

81
rophako/model/emoticons.py

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""Emoticon models."""
from flask import g, url_for
import os
import codecs
import json
import re
import config
import rophako.jsondb as JsonDB
from rophako.log import logger
_cache = {}
def load_theme():
"""Pre-load and cache the emoticon theme. This happens on startup."""
theme = config.EMOTICON_THEME
global _cache
# Cached?
if _cache:
return _cache
# Only if the theme file exists.
settings = os.path.join(config.EMOTICON_ROOT_PRIVATE, theme, "emoticons.json")
if not os.path.isfile(settings):
logger.error("Failed to load smiley theme {}: not found!")
# Try the default (tango).
theme = "tango"
settings = os.path.join(config.EMOTICON_ROOT_PRIVATE, theme, "emoticons.json")
if os.path.isfile(settings):
logger.info("Falling back to default theme: tango")
else:
# Give up.
return {}
# Read it.
fh = codecs.open(settings, "r", "utf-8")
text = fh.read()
fh.close()
try:
data = json.loads(text)
except Exception, e:
logger.error("Couldn't load JSON from emoticon file: {}".format(e))
data = {}
# Cache and return it.
_cache = data
return data
def render(message):
"""Render the emoticons into a message.
The message should already be stripped of HTML and otherwise be 'safe' to
embed on a web page. The output of this function includes `<img>` tags and
these won't work otherwise."""
# Get the smileys config.
smileys = load_theme()
# Process all smileys.
for img in sorted(smileys["map"]):
for trigger in smileys["map"][img]:
if trigger in message:
# Substitute it.
sub = """<img src="{url}" alt="{trigger}" title="{trigger}">""".format(
url="/static/smileys/{}/{}".format(config.EMOTICON_THEME, img),
trigger=trigger,
)
pattern = r'([^A-Za-z0-9:\-]|^){}([^A-Za-z0-9:\-]|$)'.format(re.escape(trigger))
result = r'\1{}\2'.format(sub)
message = re.sub(pattern, result, message)
return message

18
rophako/modules/blog.py

@ -9,6 +9,8 @@ import calendar
import rophako.model.user as User
import rophako.model.blog as Blog
import rophako.model.comment as Comment
import rophako.model.emoticons as Emoticons
from rophako.utils import template, pretty_time, login_required
from rophako.log import logger
from config import *
@ -40,6 +42,10 @@ def entry(fid):
post = Blog.get_entry(post_id)
post["post_id"] = post_id
# Render emoticons.
if post["emoticons"]:
post["body"] = Emoticons.render(post["body"])
# Get the author's information.
post["profile"] = User.get_user(uid=post["author"])
post["photo"] = User.get_picture(uid=post["author"])
@ -48,8 +54,8 @@ def entry(fid):
# Pretty-print the time.
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
# TODO: count the comments for this post
post["comment_count"] = 0
# Count the comments for this post
post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
g.info["post"] = post
return template("blog/entry.html")
@ -286,6 +292,10 @@ def partial_index():
post["post_id"] = post_id
# Render emoticons.
if post["emoticons"]:
post["body"] = Emoticons.render(post["body"])
# Get the author's information.
post["profile"] = User.get_user(uid=post["author"])
post["photo"] = User.get_picture(uid=post["author"])
@ -293,8 +303,8 @@ def partial_index():
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
# TODO: count the comments for this post
post["comment_count"] = 0
# Count the comments for this post
post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
selected.append(post)
g.info["count"] += 1

186
rophako/modules/comment.py

@ -0,0 +1,186 @@
# -*- coding: utf-8 -*-
"""Endpoints for the commenting subsystem."""
from flask import Blueprint, g, request, redirect, url_for, session, flash
import re
import time
import rophako.model.user as User
import rophako.model.comment as Comment
from rophako.utils import template, pretty_time, login_required, sanitize_name
from rophako.log import logger
from config import *
mod = Blueprint("comment", __name__, url_prefix="/comments")
## TODO: emoticon support
@mod.route("/")
def index():
return template("blog/index.html")
@mod.route("/preview", methods=["POST"])
def preview():
# Get the form fields.
form = get_comment_form(request.form)
# Trap fields.
trap1 = request.form.get("website", "x") != "http://"
trap2 = request.form.get("email", "x") != ""
if trap1 or trap2:
flash("Wanna try that again?")
return redirect(url_for("index"))
# Validate things.
if len(form["message"]) == 0:
flash("You must provide a message with your comment.")
return redirect(form["url"])
# Gravatar.
g.info["gravatar"] = Comment.gravatar(form.get("contact", ""))
g.info["preview"] = Comment.format_message(form.get("message", ""))
g.info.update(form)
return template("comment/preview.html")
@mod.route("/post", methods=["POST"])
def post():
# Get the form fields.
form = get_comment_form(request.form)
thread = sanitize_name(form["thread"])
# Gravatar?
gravatar = Comment.gravatar(form.get("contact"))
# Validate things.
if len(form["message"]) == 0:
flash("You must provide a message with your comment.")
return redirect(form["url"])
Comment.add_comment(
thread=thread,
uid=g.info["session"]["uid"],
ip=request.remote_addr,
time=int(time.time()),
image=gravatar,
name=form["name"],
subject=form["subject"],
message=form["message"],
url=form["url"],
)
# Are we subscribing to the thread?
if form.get("subscribe", "false") == "true":
email = form.get("contact", "")
if "@" in email:
Comment.add_subscriber(thread, email)
flash("You have been subscribed to future comments on this page.")
flash("Your comment has been added!")
return redirect(form["url"])
@mod.route("/delete/<thread>/<cid>")
@login_required
def delete(thread, cid):
"""Delete a comment."""
url = request.args.get("url")
Comment.delete_comment(thread, cid)
flash("Comment deleted!")
return redirect(url or url_for("index"))
@mod.route("/privacy")
def privacy():
"""The privacy policy and global unsubscribe page."""
return template("comment/privacy.html")
@mod.route("/unsubscribe", methods=["GET", "POST"])
def unsubscribe():
"""Unsubscribe an e-mail from a comment thread (or all threads)."""
# This endpoint can be called with either method. For the unsubscribe links
# inside the e-mails, it uses GET. For the global out-opt, it uses POST.
thread, email = None, None
if request.method == "POST":
thread = request.form.get("thread", "")
email = request.form.get("email", "")
# Spam check.
trap1 = request.form.get("url", "x") != "http://"
trap2 = request.form.get("message", "x") != ""
if trap1 or trap2:
flash("Wanna try that again?")
return redirect(url_for("index"))
else:
thread = request.args.get("thread", "")
email = request.args.get("who", "")
# Input validation.
if not thread:
flash("Comment thread not found.")
return redirect(url_for("index"))
if not email:
flash("E-mail address not provided.")
return redirect(url_for("index"))
# Do the unsubscribe. If thread is *, this means a global unsubscribe from
# all threads.
Comment.unsubscribe(thread, email)
g.info["thread"] = thread
g.info["email"] = email
return template("comment/unsubscribed.html")
def partial_index(thread, subject, header=True):
"""Partial template for including the index view of a comment thread."""
comments = Comment.get_comments(thread)
# Sort the comments by most recent on bottom.
sorted_cids = [ x for x in sorted(comments, key=lambda y: comments[y]["time"]) ]
sorted_comments = []
for cid in sorted_cids:
comment = comments[cid]
comment["id"] = cid
# Was the commenter logged in?
if comment["uid"] > 0:
user = User.get_user(uid=comment["uid"])
avatar = User.get_picture(uid=comment["uid"])
comment["name"] = user["name"]
comment["username"] = user["username"]
comment["image"] = avatar
# Add the pretty time.
comment["pretty_time"] = pretty_time(COMMENT_TIME_FORMAT, comment["time"])
# Format the message for display.
comment["formatted_message"] = Comment.format_message(comment["message"])
sorted_comments.append(comment)
g.info["header"] = header
g.info["thread"] = thread
g.info["subject"] = subject
g.info["url"] = request.url
g.info["comments"] = sorted_comments
g.info["photo_url"] = PHOTO_ROOT_PUBLIC
return template("comment/index.inc.html")
def get_comment_form(form):
return dict(
thread = request.form.get("thread", ""),
url = request.form.get("url", ""),
subject = request.form.get("subject", "[No Subject]"),
name = request.form.get("name", ""),
contact = request.form.get("contact", ""),
message = request.form.get("message", ""),
subscribe = request.form.get("subscribe", "false"),
)

30
rophako/modules/emoticons.py

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""Endpoints for the commenting subsystem."""
from flask import Blueprint, g
import rophako.model.emoticons as Emoticons
from rophako.utils import template
from rophako.log import logger
from config import *
mod = Blueprint("emoticons", __name__, url_prefix="/emoticons")
@mod.route("/")
def index():
"""List the available emoticons."""
theme = Emoticons.load_theme()
smileys = []
for img in sorted(theme["map"]):
smileys.append({
"img": img,
"triggers": theme["map"][img],
})
g.info["theme"] = EMOTICON_THEME
g.info["theme_name"] = theme["name"]
g.info["smileys"] = smileys
return template("emoticons/index.html")

27
rophako/utils.py

@ -7,8 +7,10 @@ import datetime
import time
import re
import importlib
import smtplib
from rophako.log import logger
from config import *
def login_required(f):
@ -52,6 +54,29 @@ def template(name, **kwargs):
return html
def send_email(to, subject, message, sender=None):
"""Send an e-mail out."""
if sender is None:
sender = MAIL_SENDER
if type(to) != list:
to = [to]
logger.info("Send email to {}".format(to))
if MAIL_METHOD == "smtp":
# Send mail with SMTP.
for email in to:
server = smtplib.SMTP(MAIL_SERVER, MAIL_PORT)
server.set_debuglevel(1)
msg = """From: {}
To: {}
Subject: {}
{}""".format(sender, email, subject, message)
server.sendmail(sender, email, msg)
server.quit()
def generate_csrf_token():
"""Generator for CSRF tokens."""
if "_csrf" not in session:
@ -82,4 +107,4 @@ def sanitize_name(name):
"""Sanitize a name that may be used in the filesystem.
Only allows numbers, letters, and some symbols."""
return re.sub(r'[^A-Za-z0-9 .-_]+', '', name)
return re.sub(r'[^A-Za-z0-9 .\-_]+', '', name)

8
rophako/www/blog/entry.inc.html

@ -67,5 +67,13 @@
{% endif %}
]
</div>
<p>
{% if from != "index" %}
{{ include_page("comment.partial_index",
thread="blog-"+post["post_id"]|string,
subject=post["subject"],
) | safe }}
{% endif %}
{% endmacro %}

2
rophako/www/blog/update.html

@ -27,7 +27,7 @@
<strong>Body:</strong><br>
<textarea cols="80" rows="12" name="body">{{ body }}</textarea><br>
<a href="/emoticons" target="_blank">Emoticon reference</a> (opens in new window)<p>
<a href="{{ url_for('emoticons.index') }}" target="_blank">Emoticon reference</a> (opens in new window)<p>
<strong>Avatar:</strong><br>
<span id="avatar-preview"></span>

90
rophako/www/comment/index.inc.html

@ -0,0 +1,90 @@
{% if header %}
<h1>Comments</h1>
{% endif %}
There {% if comments|length == 1 %}is{% else %}are{% endif %}
{{ comments|length }} comment{% if comments|length != 1 %}s{% endif %}
on this page.<p>
{% for comment in comments %}
<div class="comment">
<div class="comment-author">
{% if comment["image"] and (comment["image"].startswith('http:') or comment["image"].startswith('https:')) %}
<img src="{{ comment['image'] }}" alt="Avatar" width="96" height="96">
{% elif comment["image"] %}
<img src="{{ photo_url }}/{{ comment['image'] }}" alt="Avatar" width="96" height="96">
{% else %}
<img src="/static/avatars/default.png" alt="guest" width="96" height="96">
{% endif %}<br>
<strong>{% if comment['username'] %}{{ comment['username'] }}{% else %}guest{% endif %}</strong>
</div>
<strong>Posted on {{ comment["pretty_time"] }} by {{ comment["name"] }}.</strong><p>
{{ comment["formatted_message"]|safe }}
<div class="clear">
{% if session["login"] %}
[IP: {{ comment["ip"] }} | <a href="{{ url_for('comment.delete', thread=thread, cid=comment['id'], url=url) }}" onclick="return window.confirm('Are you sure?')">Delete</a>]
{% endif %}
</div>
</div><p>
{% endfor %}
<h2>Add a Comment</h2>
<form name="comment" action="{{ url_for('comment.preview') }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="thread" value="{{ thread }}">
<input type="hidden" name="url" value="{{ url }}">
<input type="hidden" name="subject" value="{{ subject }}">
<table border="0" cellspacing="2" cellpadding="2">
<tr>
<td align="left" valign="middle">
Your name:
</td>
<td align="left" valign="middle">
{% if session["login"] %}
<strong>{{ session["name"] }}</strong>
{% else %}
<input type="text" size="40" name="name">
{% endif %}
</td>
</tr>
<tr>
<td align="left" valign="middle">
Your Email:
</td>
<td align="left" valign="middle">
<input type="text" size="40" name="contact"> <small>(optional)</small>
</td>
</tr>
<tr>
<td align="left" valign="top">
Message:
</td>
<td align="left" valign="top">
<textarea cols="40" rows="8" name="message" style="width: 100%"></textarea><br>
<small>You can use <a href="{{ url_for('emoticons.index') }}" target="_blank">emoticons</a>
in your comment. <em>(opens in a new window)</em></small>
</td>
</tr>
<tr>
<td colspan="2" align="left" valign="top">
<label>
<input type="checkbox" name="subscribe" value="true">
Notify me of future comments on this page via e-mail
(<a href="{{ url_for('comment.privacy') }}" target="_blank">Privacy Policy</a>)
</label>
</td>
</tr>
</table><p>
<div style="display: none">
If you can see this, don't touch the following fields.<br>
<input type="text" name="website" value="http://"><br>
<input type="text" name="email" value="">
</div>
<button type="submit">Leave Comment</button>
</form>

32
rophako/www/comment/preview.html

@ -0,0 +1,32 @@
{% extends "layout.html" %}
{% block title %}Comment Preview{% endblock %}
{% block content %}
<h1>Comment Preview</h1>
This is a preview of what your comment is going to look like once posted.<p>
<hr><p>
{{ preview|safe }}<p>
<hr><p>
{% if subscribe == "true" and contact %}
You will be subscribed to future comments on this thread. Notification
e-mails will be sent to {{ contact }}.<p>
{% endif %}
<form name="preview" action="{{ url_for('comment.post') }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="thread" value="{{ thread }}">
<input type="hidden" name="url" value="{{ url }}">
<input type="hidden" name="subject" value="{{ subject }}">
<input type="hidden" name="name" value="{{ name }}">
<input type="hidden" name="message" value="{{ message }}">
<input type="hidden" name="contact" value="{{ contact }}">
<input type="hidden" name="subscribe" value="{{ subscribe }}">
<button type="submit">Publish Comment</button>
</form>
{% endblock %}

61
rophako/www/comment/privacy.html

@ -0,0 +1,61 @@
{% extends "layout.html" %}
{% block title %}Comment Subscriptions{% endblock %}
{% block content %}
<h1>Subscribing to Comments</h1>
When posting a comment on this site, you can optionally subscribe to future
comments on the same page (so you can get an e-mail notification when somebody
answers your questions, for example).<p>
You can unsubscribe from these e-mails in the future by clicking a link in the
e-mail. Or, you can unsubscribe from all comment threads by entering your
e-mail address in the form below.<p>
<h2>Privacy Policy</h2>
<ul>
<li>
Your e-mail address that you use when you post the comment will only be
used for sending you notifications via e-mail when somebody else replies
to the comment thread and for showing a
<a href="http://www.gravatar.com/" target="_blank">Gravatar</a> next to
your comment.
</li>
<li>
Your e-mail will not be visible to anybody else on this site.
</li>
<li>
Your e-mail won't be given to any spammers so you don't need to worry
about junk mail.
</li>
<li>
You can unsubscribe from individual comment threads by using the link
provided in the notification e-mail. You can unsubscribe from ALL
threads by using the form on this page.
</li>
</ul>
<h2>Unsubscribe from All Comment Threads</h2>
<form name="unsubscribe" action="{{ url_for('comment.unsubscribe') }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<input type="hidden" name="thread" value="*">
Enter the e-mail address to be unsubscribed from all threads:<br>
<input type="email" size="40" name="email"><p>
<button type="submit">Unsubscribe</button>
<div style="display: none">
If you can see this, do not touch these fields.<br>
<input type="text" name="url" value="http://"><br>
<input type="text" name="message" value="">
</div>
</form>
{% endblock %}

14
rophako/www/comment/unsubscribed.html

@ -0,0 +1,14 @@
{% extends "layout.html" %}
{% block title %}Comment Subscriptions{% endblock %}
{% block content %}
<h1>You have been unsubscribed</h1>
The e-mail address <strong>{{ email }}</strong> has been unsubscribed
{% if thread == "*" %}
from all comment threads on this site.
{% else %}
from the comment thread "{{ thread }}".
{% endif %}
{% endblock %}

30
rophako/www/emoticons/index.html

@ -0,0 +1,30 @@
{% extends "layout.html" %}
{% block title %}Emoticons{% endblock %}
{% block content %}
<h1>Emoticon Theme: {{ theme_name }}</h1>
<table class="table" cellspacing="0" cellpadding="2">
<thead>
<tr>
<th>Emoticon</th>
<th>Trigger Text</th>
</tr>
</thead>
<tbody>
{% for img in smileys %}
<tr>
<td align="center" valign="middle">
<img src="/static/smileys/{{ theme }}/{{ img['img'] }}">
</td>
<td align="left" valign="middle">
{% for trigger in img['triggers'] %}
{{ trigger }}&nbsp;&nbsp;&nbsp;&nbsp;
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

BIN
rophako/www/static/smileys/tango/act-up.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

BIN
rophako/www/static/smileys/tango/airplane.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

BIN
rophako/www/static/smileys/tango/alien.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

BIN
rophako/www/static/smileys/tango/angel.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

BIN
rophako/www/static/smileys/tango/angry.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

BIN
rophako/www/static/smileys/tango/arrogant.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

BIN
rophako/www/static/smileys/tango/bad.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

BIN
rophako/www/static/smileys/tango/bashful.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

BIN
rophako/www/static/smileys/tango/beat-up.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

BIN
rophako/www/static/smileys/tango/beauty.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

BIN
rophako/www/static/smileys/tango/beer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

BIN
rophako/www/static/smileys/tango/blowkiss.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

BIN
rophako/www/static/smileys/tango/bomb.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

BIN
rophako/www/static/smileys/tango/bowl.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

BIN
rophako/www/static/smileys/tango/boy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

BIN
rophako/www/static/smileys/tango/brb.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

BIN
rophako/www/static/smileys/tango/bye.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

BIN
rophako/www/static/smileys/tango/cake.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

BIN
rophako/www/static/smileys/tango/call-me.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

BIN
rophako/www/static/smileys/tango/camera.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

BIN
rophako/www/static/smileys/tango/can.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

BIN
rophako/www/static/smileys/tango/car.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

BIN
rophako/www/static/smileys/tango/cat.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

BIN
rophako/www/static/smileys/tango/chicken.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

BIN
rophako/www/static/smileys/tango/clap.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

BIN
rophako/www/static/smileys/tango/clock.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

BIN
rophako/www/static/smileys/tango/cloudy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

BIN
rophako/www/static/smileys/tango/clover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

BIN
rophako/www/static/smileys/tango/clown.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

BIN
rophako/www/static/smileys/tango/coffee.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

BIN
rophako/www/static/smileys/tango/coins.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

BIN
rophako/www/static/smileys/tango/computer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

BIN
rophako/www/static/smileys/tango/confused.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

BIN
rophako/www/static/smileys/tango/console.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

BIN
rophako/www/static/smileys/tango/cow.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

BIN
rophako/www/static/smileys/tango/cowboy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

BIN
rophako/www/static/smileys/tango/crying.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

BIN
rophako/www/static/smileys/tango/curl-lip.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

BIN
rophako/www/static/smileys/tango/curse.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

BIN
rophako/www/static/smileys/tango/cute.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

BIN
rophako/www/static/smileys/tango/dance.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

BIN
rophako/www/static/smileys/tango/dazed.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

BIN
rophako/www/static/smileys/tango/desire.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

BIN
rophako/www/static/smileys/tango/devil.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

BIN
rophako/www/static/smileys/tango/disapointed.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

BIN
rophako/www/static/smileys/tango/disdain.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

BIN
rophako/www/static/smileys/tango/doctor.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

BIN
rophako/www/static/smileys/tango/dog.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

BIN
rophako/www/static/smileys/tango/doh.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

BIN
rophako/www/static/smileys/tango/dont-know.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

BIN
rophako/www/static/smileys/tango/drink.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

BIN
rophako/www/static/smileys/tango/drool.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

BIN
rophako/www/static/smileys/tango/eat.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

BIN
rophako/www/static/smileys/tango/embarrassed.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

328
rophako/www/static/smileys/tango/emoticons.json

@ -0,0 +1,328 @@
{
"map" : {
"neutral.png" : [
":-|",
":|"
],
"rain.png" : [
"(st)"
],
"mail.png" : [
"(E)",
"(e)"
],
"tongue.png" : [
":-P",
":P",
":-p",
":p"
],
"computer.png" : [
"(co)"
],
"love-over.png" : [
"(U)",
"(u)"
],
"good.png" : [
"(Y)",
"(y)"
],
"in-love.png" : [
"*IN",
"LOVE*"
],
"coffee.png" : [
"(C)",
"(c)"
],
"secret.png" : [
":-X",
":-x"
],
"wilt.png" : [
"|-0"
],
"yin-yang.png" : [
"(%)"
],
"yawn.png" : [
"|-)"
],
"glasses-nerdy.png" : [
"8-|"
],
"laugh.png" : [
":-D",
":D",
":d",
":-d"
],
"skywalker.png" : [
"C:-)",
"c:-)",
"C:)",
"c:)"
],
"car.png" : [
"(au)"
],
"pizza.png" : [
"(pi)"
],
"thunder.png" : [
"(li)"
],
"teeth.png" : [
"8o|"
],
"moon.png" : [
"(S)"
],
"smile-big.png" : [
":-))",
":))"
],
"sarcastic.png" : [
"^o)"
],
"confused.png" : [
":-S",
":S",
":s",
":-s"
],
"film.png" : [
"(~)"
],
"party.png" : [
"<:o)"
],
"turtle.png" : [
"(tu)"
],
"beer.png" : [
"(B)",
"(b)"
],
"clock.png" : [
"(O)",
"(o)"
],
"plate.png" : [
"(pl)"
],
"highfive.png" : [
"(h5)"
],
"angry.png" : [
":-@",
":@",
">:o",
">:O"
],
"rose.png" : [
"(F)",
"(f)"
],
"sick.png" : [
":-!",
":!",
"+o(",
"+O("
],
"victory.png" : [