Add Comments and Emoticons modules
|
@ -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 ##
|
||||
################################################################################
|
||||
|
@ -64,3 +80,13 @@ PHOTO_TIME_FORMAT = BLOG_TIME_FORMAT
|
|||
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.
|
||||
|
||||
################################################################################
|
||||
## 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 = ""
|
|
@ -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():
|
||||
|
|
|
@ -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
Normal file
|
@ -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'&', '&', message)
|
||||
message = re.sub(r'<', '<', message)
|
||||
message = re.sub(r'>', '>', message)
|
||||
message = re.sub(r'"', '"', message)
|
||||
message = re.sub(r"'", ''', 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
Normal file
|
@ -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
|
|
@ -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
Normal file
|
@ -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
Normal file
|
@ -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")
|
|
@ -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)
|
|
@ -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 %}
|
|
@ -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
Normal file
|
@ -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
Normal file
|
@ -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
Normal file
|
@ -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
Normal file
|
@ -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
Normal file
|
@ -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 }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
BIN
rophako/www/static/smileys/tango/act-up.png
Normal file
After Width: | Height: | Size: 934 B |
BIN
rophako/www/static/smileys/tango/airplane.png
Normal file
After Width: | Height: | Size: 868 B |
BIN
rophako/www/static/smileys/tango/alien.png
Normal file
After Width: | Height: | Size: 999 B |
BIN
rophako/www/static/smileys/tango/angel.png
Normal file
After Width: | Height: | Size: 974 B |
BIN
rophako/www/static/smileys/tango/angry.png
Normal file
After Width: | Height: | Size: 942 B |
BIN
rophako/www/static/smileys/tango/arrogant.png
Normal file
After Width: | Height: | Size: 920 B |
BIN
rophako/www/static/smileys/tango/bad.png
Normal file
After Width: | Height: | Size: 858 B |
BIN
rophako/www/static/smileys/tango/bashful.png
Normal file
After Width: | Height: | Size: 920 B |
BIN
rophako/www/static/smileys/tango/beat-up.png
Normal file
After Width: | Height: | Size: 942 B |
BIN
rophako/www/static/smileys/tango/beauty.png
Normal file
After Width: | Height: | Size: 936 B |
BIN
rophako/www/static/smileys/tango/beer.png
Normal file
After Width: | Height: | Size: 936 B |
BIN
rophako/www/static/smileys/tango/blowkiss.png
Normal file
After Width: | Height: | Size: 903 B |
BIN
rophako/www/static/smileys/tango/bomb.png
Normal file
After Width: | Height: | Size: 877 B |
BIN
rophako/www/static/smileys/tango/bowl.png
Normal file
After Width: | Height: | Size: 865 B |
BIN
rophako/www/static/smileys/tango/boy.png
Normal file
After Width: | Height: | Size: 838 B |
BIN
rophako/www/static/smileys/tango/brb.png
Normal file
After Width: | Height: | Size: 757 B |
BIN
rophako/www/static/smileys/tango/bye.png
Normal file
After Width: | Height: | Size: 933 B |
BIN
rophako/www/static/smileys/tango/cake.png
Normal file
After Width: | Height: | Size: 865 B |
BIN
rophako/www/static/smileys/tango/call-me.png
Normal file
After Width: | Height: | Size: 937 B |
BIN
rophako/www/static/smileys/tango/camera.png
Normal file
After Width: | Height: | Size: 777 B |
BIN
rophako/www/static/smileys/tango/can.png
Normal file
After Width: | Height: | Size: 752 B |
BIN
rophako/www/static/smileys/tango/car.png
Normal file
After Width: | Height: | Size: 998 B |
BIN
rophako/www/static/smileys/tango/cat.png
Normal file
After Width: | Height: | Size: 970 B |
BIN
rophako/www/static/smileys/tango/chicken.png
Normal file
After Width: | Height: | Size: 876 B |
BIN
rophako/www/static/smileys/tango/clap.png
Normal file
After Width: | Height: | Size: 916 B |
BIN
rophako/www/static/smileys/tango/clock.png
Normal file
After Width: | Height: | Size: 939 B |
BIN
rophako/www/static/smileys/tango/cloudy.png
Normal file
After Width: | Height: | Size: 810 B |
BIN
rophako/www/static/smileys/tango/clover.png
Normal file
After Width: | Height: | Size: 914 B |
BIN
rophako/www/static/smileys/tango/clown.png
Normal file
After Width: | Height: | Size: 956 B |
BIN
rophako/www/static/smileys/tango/coffee.png
Normal file
After Width: | Height: | Size: 940 B |
BIN
rophako/www/static/smileys/tango/coins.png
Normal file
After Width: | Height: | Size: 905 B |
BIN
rophako/www/static/smileys/tango/computer.png
Normal file
After Width: | Height: | Size: 770 B |
BIN
rophako/www/static/smileys/tango/confused.png
Normal file
After Width: | Height: | Size: 918 B |
BIN
rophako/www/static/smileys/tango/console.png
Normal file
After Width: | Height: | Size: 893 B |
BIN
rophako/www/static/smileys/tango/cow.png
Normal file
After Width: | Height: | Size: 911 B |
BIN
rophako/www/static/smileys/tango/cowboy.png
Normal file
After Width: | Height: | Size: 1006 B |
BIN
rophako/www/static/smileys/tango/crying.png
Normal file
After Width: | Height: | Size: 937 B |
BIN
rophako/www/static/smileys/tango/curl-lip.png
Normal file
After Width: | Height: | Size: 914 B |
BIN
rophako/www/static/smileys/tango/curse.png
Normal file
After Width: | Height: | Size: 915 B |
BIN
rophako/www/static/smileys/tango/cute.png
Normal file
After Width: | Height: | Size: 910 B |
BIN
rophako/www/static/smileys/tango/dance.png
Normal file
After Width: | Height: | Size: 946 B |
BIN
rophako/www/static/smileys/tango/dazed.png
Normal file
After Width: | Height: | Size: 914 B |
BIN
rophako/www/static/smileys/tango/desire.png
Normal file
After Width: | Height: | Size: 959 B |
BIN
rophako/www/static/smileys/tango/devil.png
Normal file
After Width: | Height: | Size: 988 B |
BIN
rophako/www/static/smileys/tango/disapointed.png
Normal file
After Width: | Height: | Size: 886 B |
BIN
rophako/www/static/smileys/tango/disdain.png
Normal file
After Width: | Height: | Size: 939 B |
BIN
rophako/www/static/smileys/tango/doctor.png
Normal file
After Width: | Height: | Size: 961 B |
BIN
rophako/www/static/smileys/tango/dog.png
Normal file
After Width: | Height: | Size: 952 B |
BIN
rophako/www/static/smileys/tango/doh.png
Normal file
After Width: | Height: | Size: 901 B |
BIN
rophako/www/static/smileys/tango/dont-know.png
Normal file
After Width: | Height: | Size: 910 B |
BIN
rophako/www/static/smileys/tango/drink.png
Normal file
After Width: | Height: | Size: 871 B |
BIN
rophako/www/static/smileys/tango/drool.png
Normal file
After Width: | Height: | Size: 925 B |
BIN
rophako/www/static/smileys/tango/eat.png
Normal file
After Width: | Height: | Size: 931 B |
BIN
rophako/www/static/smileys/tango/embarrassed.png
Normal file
After Width: | Height: | Size: 909 B |
328
rophako/www/static/smileys/tango/emoticons.json
Normal file
|
@ -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" : [
|
||||
"*BRAVO*",
|
||||
":BRAVO:",
|
||||
":bravo:",
|
||||
":clapping:"
|
||||
],
|
||||
"angel.png" : [
|
||||
"O:-)",
|
||||
"O:)",
|
||||
"(A)",
|
||||
"(a)"
|
||||
],
|
||||
"thinking.png" : [
|
||||
"*-)"
|
||||
],
|
||||
"island.png" : [
|
||||
"(ip)"
|
||||
],
|
||||
"smile.png" : [
|
||||
":-)",
|
||||
":)"
|
||||
],
|
||||
"rose-dead.png" : [
|
||||
"(W)",
|
||||
"(w)"
|
||||
],
|
||||
"msn.png" : [
|
||||
"(M)",
|
||||
"(m)"
|
||||
],
|
||||
"umbrella.png" : [
|
||||
"(um)"
|
||||
],
|
||||
"hug-left.png" : [
|
||||
"({)"
|
||||
],
|
||||
"shock.png" : [
|
||||
":-O",
|
||||
":-o",
|
||||
":O",
|
||||
":o",
|
||||
"=-O",
|
||||
"=-o"
|
||||
],
|
||||
"rotfl.png" : [
|
||||
"*ROFL*",
|
||||
"*rofl*"
|
||||
],
|
||||
"airplane.png" : [
|
||||
"(ap)"
|
||||
],
|
||||
"sun.png" : [
|
||||
"(#)"
|
||||
],
|
||||
"goat.png" : [
|
||||
"(nah)"
|
||||
],
|
||||
"crying.png" : [
|
||||
":'("
|
||||
],
|
||||
"sad.png" : [
|
||||
":-(",
|
||||
":("
|
||||
],
|
||||
"cat.png" : [
|
||||
"(@)"
|
||||
],
|
||||
"bomb.png" : [
|
||||
"@="
|
||||
],
|
||||
"glasses-cool.png" : [
|
||||
"(H)",
|
||||
"(h)"
|
||||
],
|
||||
"hypnotized.png" : [
|
||||
"%)",
|
||||
"%-)"
|
||||
],
|
||||
"girl.png" : [
|
||||
"(X)",
|
||||
"(x)"
|
||||
],
|
||||
"bowl.png" : [
|
||||
"(||)"
|
||||
],
|
||||
"bad.png" : [
|
||||
"(N)",
|
||||
"(n)"
|
||||
],
|
||||
"mobile.png" : [
|
||||
"(mp)"
|
||||
],
|
||||
"monkey.png" : [
|
||||
":-(|)"
|
||||
],
|
||||
"love.png" : [
|
||||
"(L)",
|
||||
"(l)"
|
||||
],
|
||||
"phone.png" : [
|
||||
"(T)",
|
||||
"(t)"
|
||||
],
|
||||
"quiet.png" : [
|
||||
":-#"
|
||||
],
|
||||
"eyeroll.png" : [
|
||||
"8-)",
|
||||
"8)"
|
||||
],
|
||||
"vampire.png" : [
|
||||
":-[",
|
||||
":["
|
||||
],
|
||||
"dog.png" : [
|
||||
"(&)"
|
||||
],
|
||||
"camera.png" : [
|
||||
"(P)",
|
||||
"(p)"
|
||||
],
|
||||
"dance.png" : [
|
||||
"*DANCE*",
|
||||
":dance:"
|
||||
],
|
||||
"snail.png" : [
|
||||
"(sn)"
|
||||
],
|
||||
"soccerball.png" : [
|
||||
"(so)"
|
||||
],
|
||||
"star.png" : [
|
||||
"(*)"
|
||||
],
|
||||
"lamp.png" : [
|
||||
"(I)",
|
||||
"(i)"
|
||||
],
|
||||
"sheep.png" : [
|
||||
"(bah)"
|
||||
],
|
||||
"boy.png" : [
|
||||
"(Z)",
|
||||
"(z)"
|
||||
],
|
||||
"dont-know.png" : [
|
||||
":^)"
|
||||
],
|
||||
"rainbow.png" : [
|
||||
"(R)",
|
||||
"(r)"
|
||||
],
|
||||
"coins.png" : [
|
||||
"(mo)"
|
||||
],
|
||||
"hug-right.png" : [
|
||||
"(})"
|
||||
],
|
||||
"embarrassed.png" : [
|
||||
":-$",
|
||||
":$"
|
||||
],
|
||||
"devil.png" : [
|
||||
"(6)"
|
||||
],
|
||||
"kiss.png" : [
|
||||
":-*",
|
||||
"(K)",
|
||||
"(k)"
|
||||
],
|
||||
"brb.png" : [
|
||||
"(brb)"
|
||||
],
|
||||
"present.png" : [
|
||||
"(G)",
|
||||
"(g)"
|
||||
],
|
||||
"cake.png" : [
|
||||
"(^)"
|
||||
],
|
||||
"drink.png" : [
|
||||
"(D)",
|
||||
"(d)"
|
||||
],
|
||||
"musical-note.png" : [
|
||||
"(8)"
|
||||
],
|
||||
"wink.png" : [
|
||||
";-)",
|
||||
";)"
|
||||
]
|
||||
},
|
||||
"source" : "http://digsbies.org/site/content/project/tango-emoticons-big-pack",
|
||||
"name" : "Tango"
|
||||
}
|
86
rophako/www/static/smileys/tango/emoticons.txt
Normal file
|
@ -0,0 +1,86 @@
|
|||
smile.png :-) :) +) =) :smile:
|
||||
smile-big.png *LOL* :-)) :)) =)) +)) :-))) :))) LOL lol LOL! lol! :lol:
|
||||
laugh.png :-D :D :d :-d +D =D :biggrin:
|
||||
wink.png ;-) ;) ^_~ :wink:
|
||||
shock.png :-O :-o :O :o
|
||||
tongue.png :-P :P :-p :p +P =P +p =p :-b :b +b =b :tongue:
|
||||
glasses-cool.png (H) (h)
|
||||
angry.png :-@ :@ >:o >:O >+O >:o >+o :angry:
|
||||
embarrassed.png :-$ :$
|
||||
confused.png :-S :S :s :-s
|
||||
sad.png :-( :( +( =( :-(( :(( +(( =(( :sad:
|
||||
crying.png :'(
|
||||
neutral.png :-| :|
|
||||
devil.png ]:-> }:-> ]:> }:> >:-] >:] (6) :diablo: *DIABLO*
|
||||
angel.png O:-) O:) O+) O=) 0:-) 0:) 0+) 0=) (A) (a)
|
||||
love.png (L) (l)
|
||||
love-over.png (U) (u)
|
||||
msn.png (M) (m)
|
||||
cat.png (@)
|
||||
dog.png (&)
|
||||
moon.png (S)
|
||||
star.png (*)
|
||||
film.png (~)
|
||||
musical-note.png (8)
|
||||
mail.png (E) (e)
|
||||
rose.png @}->-- @}-:-- @>}--,-`--- (F) (f)
|
||||
rose-dead.png (W) (w)
|
||||
clock.png (O) (o)
|
||||
kiss.png :-* (K) (k)
|
||||
secret.png :-X
|
||||
present.png (G) (g)
|
||||
cake.png (^)
|
||||
camera.png (P) (p)
|
||||
lamp.png (I) (i)
|
||||
coffee.png (C) (c)
|
||||
phone.png (T) (t)
|
||||
hug-left.png ({)
|
||||
hug-right.png (})
|
||||
beer.png *DRINK* DRINK :drink: (B) (b)
|
||||
drink.png (D) (d)
|
||||
boy.png (Z) (z)
|
||||
girl.png (X) (x)
|
||||
good.png *THUMBS UP* (Y) (y)
|
||||
bad.png (N) (n)
|
||||
vampire.png :-[ :[ ;'> ;-. :blush:
|
||||
goat.png (nah)
|
||||
sun.png (#)
|
||||
rainbow.png (R) (r)
|
||||
quiet.png :-#
|
||||
teeth.png 8o|
|
||||
glasses-nerdy.png 8-|
|
||||
sarcastic.png ^o)
|
||||
sick.png :-! :! +o( :-~ ;-~ :(~ +(~ =(~ :bad:
|
||||
snail.png (sn)
|
||||
turtle.png (tu)
|
||||
plate.png (pl)
|
||||
bowl.png (||)
|
||||
pizza.png (pi)
|
||||
soccerball.png (so)
|
||||
car.png (au)
|
||||
airplane.png (ap)
|
||||
umbrella.png (um)
|
||||
island.png (ip)
|
||||
computer.png (co)
|
||||
mobile.png (mp)
|
||||
brb.png (brb)
|
||||
rain.png (st)
|
||||
highfive.png (h5)
|
||||
coins.png (mo)
|
||||
sheep.png (bah)
|
||||
dont-know.png :^)
|
||||
thinking.png *-)
|
||||
thunder.png (li)
|
||||
party.png <:o)
|
||||
eyeroll.png 8-) 8) B) :COOL: :cool: COOL cool COOL! COOL!! COOL!!!
|
||||
yawn.png |-)
|
||||
skywalker.png C:-) c:-) C:) c:)
|
||||
monkey.png :-(|)
|
||||
yin-yang.png (%)
|
||||
wilt.png *TIRED* |-0 :boredom:
|
||||
bomb.png @=
|
||||
hypnotized.png %) %-) :-$ :$ :wacko: :WACKO:
|
||||
rotfl.png *ROFL* :ROFL: :rofl: ROFL ROFL! rofl :-)))) :-))))) :-)))))) :)))) :))))) :)))))) =)))) =))))) =))))))
|
||||
victory.png *BRAVO* :BRAVO: :bravo: :clapping:
|
||||
dance.png *DANCE* :dance:
|
||||
in-love.png *IN LOVE*
|
BIN
rophako/www/static/smileys/tango/excruciating.png
Normal file
After Width: | Height: | Size: 977 B |
BIN
rophako/www/static/smileys/tango/eyeroll.png
Normal file
After Width: | Height: | Size: 911 B |
BIN
rophako/www/static/smileys/tango/film.png
Normal file
After Width: | Height: | Size: 945 B |
BIN
rophako/www/static/smileys/tango/fingers-crossed.png
Normal file
After Width: | Height: | Size: 955 B |
BIN
rophako/www/static/smileys/tango/flag.png
Normal file
After Width: | Height: | Size: 624 B |
BIN
rophako/www/static/smileys/tango/foot-in-mouth.png
Normal file
After Width: | Height: | Size: 929 B |
BIN
rophako/www/static/smileys/tango/freaked-out.png
Normal file
After Width: | Height: | Size: 901 B |
BIN
rophako/www/static/smileys/tango/ghost.png
Normal file
After Width: | Height: | Size: 879 B |
BIN
rophako/www/static/smileys/tango/giggle.png
Normal file
After Width: | Height: | Size: 909 B |
BIN
rophako/www/static/smileys/tango/girl.png
Normal file
After Width: | Height: | Size: 741 B |
BIN
rophako/www/static/smileys/tango/glasses-cool.png
Normal file
After Width: | Height: | Size: 938 B |
BIN
rophako/www/static/smileys/tango/glasses-nerdy.png
Normal file
After Width: | Height: | Size: 987 B |
BIN
rophako/www/static/smileys/tango/go-away.png
Normal file
After Width: | Height: | Size: 918 B |
BIN
rophako/www/static/smileys/tango/goat.png
Normal file
After Width: | Height: | Size: 921 B |
BIN
rophako/www/static/smileys/tango/good.png
Normal file
After Width: | Height: | Size: 804 B |
BIN
rophako/www/static/smileys/tango/hammer.png
Normal file
After Width: | Height: | Size: 832 B |
BIN
rophako/www/static/smileys/tango/handcuffs.png
Normal file
After Width: | Height: | Size: 969 B |
BIN
rophako/www/static/smileys/tango/handshake.png
Normal file
After Width: | Height: | Size: 954 B |
BIN
rophako/www/static/smileys/tango/highfive.png
Normal file
After Width: | Height: | Size: 998 B |
BIN
rophako/www/static/smileys/tango/hug-left.png
Normal file
After Width: | Height: | Size: 945 B |
BIN
rophako/www/static/smileys/tango/hug-right.png
Normal file
After Width: | Height: | Size: 943 B |
BIN
rophako/www/static/smileys/tango/hungry.png
Normal file
After Width: | Height: | Size: 931 B |
BIN
rophako/www/static/smileys/tango/hypnotized.png
Normal file
After Width: | Height: | Size: 923 B |
BIN
rophako/www/static/smileys/tango/in-love.png
Normal file
After Width: | Height: | Size: 951 B |
BIN
rophako/www/static/smileys/tango/island.png
Normal file
After Width: | Height: | Size: 944 B |
BIN
rophako/www/static/smileys/tango/jump.png
Normal file
After Width: | Height: | Size: 922 B |
BIN
rophako/www/static/smileys/tango/kiss.png
Normal file
After Width: | Height: | Size: 921 B |
BIN
rophako/www/static/smileys/tango/knife.png
Normal file
After Width: | Height: | Size: 853 B |