357 lines
12 KiB
Python
357 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
import random
|
|
import functools
|
|
import collections
|
|
import datetime
|
|
import peewee
|
|
import flask
|
|
|
|
import io
|
|
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
|
|
|
#import poobrains
|
|
from poobrains import app
|
|
import poobrains.helpers
|
|
import poobrains.rendering
|
|
import poobrains.storage
|
|
import poobrains.form
|
|
import poobrains.auth
|
|
import poobrains.tagging
|
|
|
|
|
|
class Comment(poobrains.auth.Administerable):
|
|
|
|
class Meta:
|
|
|
|
abstract = False
|
|
order_by = ['created']
|
|
|
|
model = poobrains.storage.fields.CharField()
|
|
handle = poobrains.storage.fields.CharField()
|
|
reply_to = poobrains.storage.fields.ForeignKeyField('self', null=True)
|
|
created = poobrains.storage.fields.DateTimeField(default=datetime.datetime.now, null=False)
|
|
author = poobrains.storage.fields.CharField()
|
|
text = poobrains.storage.fields.TextField()
|
|
|
|
def __setattr__(self, name, value):
|
|
|
|
if name == 'model':
|
|
|
|
valid_models = Commentable.class_children_keyed()
|
|
if not value in valid_models:
|
|
raise AttributeError(f'Invalid model "{value}" for Comment.')
|
|
|
|
super(Comment, self).__setattr__(name, value)
|
|
|
|
|
|
def thread(self):
|
|
#TODO: Move creating trees into a stored procedure for performance at some point
|
|
thread = collections.OrderedDict()
|
|
|
|
children = Comment.select().where(Comment.reply_to == self)
|
|
|
|
for child in children:
|
|
thread[child] = child.thread()
|
|
|
|
return thread
|
|
|
|
def child_comments(self):
|
|
return Comment.select().where(Comment.reply_to == self)
|
|
|
|
|
|
def reply_form(self):
|
|
|
|
children = Commentable.class_children_keyed()
|
|
if self.model in children:
|
|
|
|
model = Commentable.class_children_keyed()[self.model]
|
|
comments_enabled = model.load(self.handle).comments_enabled
|
|
|
|
if comments_enabled:
|
|
|
|
try:
|
|
self.permissions['create'].check(flask.g.user)
|
|
return CommentForm(self.model, self.handle, reply_to=self)
|
|
|
|
except poobrains.auth.AccessDenied:
|
|
return False
|
|
|
|
return poobrains.rendering.RenderString("Commenting is disabled.")
|
|
|
|
raise Exception("Bork")
|
|
|
|
|
|
@poobrains.auth.User.on_profile
|
|
class Commentable(poobrains.tagging.Taggable):
|
|
|
|
class Meta:
|
|
abstract = True
|
|
order_by = ['-date']
|
|
|
|
comments_enabled = poobrains.storage.fields.BooleanField(default=True, verbose_name=u'Enable comments')
|
|
notify_owner = poobrains.storage.fields.BooleanField(default=True, verbose_name='Notify owner', help_text='Whether to notify the owner of comments')
|
|
date = poobrains.storage.fields.DateTimeField(default=datetime.datetime.now, verbose_name='Date', help_text='Date of publication, current time if left empty')
|
|
|
|
@property
|
|
def date_pretty(self):
|
|
if isinstance(self.date, datetime.datetime):
|
|
return self.date.strftime('%a %b %d %Y - %H:%M:%S')
|
|
return 'Lost in Time'
|
|
|
|
@property
|
|
def comments(self):
|
|
return Comment.select().where(Comment.model == self.__class__.__name__, Comment.handle == self.handle_string)
|
|
|
|
|
|
@property
|
|
def comments_threaded(self):
|
|
|
|
comments_threaded = collections.OrderedDict()
|
|
|
|
try:
|
|
Comment.permissions['read'].check(flask.g.user)
|
|
for comment in self.comments.where(Comment.reply_to == None): # iterate through root comments
|
|
comments_threaded[comment] = comment.thread()
|
|
|
|
|
|
except poobrains.auth.AccessDenied:
|
|
pass # No point loading shit this user isn't allowed to render anyways.
|
|
|
|
return comments_threaded
|
|
|
|
|
|
def comment_form(self, reply_to=None):
|
|
|
|
if self.comments_enabled:
|
|
|
|
try:
|
|
Comment.permissions['create'].check(flask.g.user) # no form for users who aren't allowed to comment
|
|
return CommentForm(instance=self, reply_to=reply_to)
|
|
|
|
except poobrains.auth.AccessDenied:
|
|
return poobrains.rendering.RenderString("You are not allowed to post comments.")
|
|
|
|
return poobrains.rendering.RenderString("Commenting is disabled.")
|
|
|
|
|
|
class CommentForm(poobrains.form.Form):
|
|
|
|
instance = None # Commentable instance the comment is going to be associated to
|
|
reply_to = poobrains.form.fields.Value()
|
|
author = poobrains.form.fields.Text(required=True, placeholder='Yer name')
|
|
text = poobrains.form.fields.TextArea(required=True, placeholder='Yer mutterings')
|
|
submit = poobrains.form.Button('submit', label='Send comment')
|
|
|
|
def __init__(self, model=None, instance_handle=None, instance=None, **kwargs):
|
|
|
|
if not isinstance(instance, Commentable):
|
|
|
|
assert model and instance_handle, "Either instance (a Commentable instance) or model AND handle must be passed."
|
|
|
|
cls = Commentable.class_children_keyed()[model]
|
|
instance = cls.load(instance_handle)
|
|
|
|
reply_to = kwargs.pop('reply_to') if 'reply_to' in kwargs else None
|
|
if isinstance(reply_to, int):
|
|
reply_to = Comment.load(reply_to)
|
|
super(CommentForm, self).__init__(**kwargs)
|
|
|
|
self.instance = instance
|
|
self.fields['reply_to'].value = reply_to
|
|
|
|
self.action = "/comment/%s/%s" % (self.instance.__class__.__name__, self.instance.handle_string) # FIXME: This is shit. Maybe we want to teach Pooprint.get_view_url handling extra parameters from the URL?
|
|
if reply_to:
|
|
self.action += "/%d" % reply_to.id
|
|
|
|
|
|
def process(self, submit):
|
|
|
|
self.instance.permissions['read'].check(flask.g.user)
|
|
|
|
iteration_limit = 10
|
|
for i in range(0, iteration_limit):
|
|
name = poobrains.helpers.random_string_light(16).lower()
|
|
if not Challenge.select().where(Challenge.name == name).count():
|
|
break
|
|
elif i == iteration_limit - 1: # means loop ran through without finding a free challenge name
|
|
flask.flash(u"I'm sorry Dave. I'm afraid I can't do that.")
|
|
return flask.redirect(self.instance.url('full'))
|
|
|
|
challenge = Challenge()
|
|
challenge.name = name
|
|
challenge.model = self.instance.__class__.__name__
|
|
challenge.handle = self.instance.handle_string
|
|
challenge.reply_to = self.fields['reply_to'].value
|
|
challenge.author = self.fields['author'].value
|
|
challenge.text = self.fields['text'].value
|
|
|
|
challenge.save()
|
|
|
|
return flask.redirect(challenge.url('full'))
|
|
|
|
app.site.add_view(CommentForm, '/comment/<string:model>/<string:instance_handle>', mode='full')
|
|
app.site.add_view(CommentForm, '/comment/<string:model>/<string:instance_handle>/<int:reply_to>', mode='full')
|
|
|
|
|
|
class Challenge(poobrains.storage.Named):
|
|
|
|
class Meta:
|
|
|
|
modes = collections.OrderedDict([('full', 'read'), ('raw', 'read')])
|
|
|
|
|
|
title = 'Fuck bots, get bugs'
|
|
captcha = poobrains.storage.fields.CharField(default=functools.partial(poobrains.helpers.random_string_light, 6))
|
|
model = poobrains.storage.fields.CharField()
|
|
handle = poobrains.storage.fields.CharField()
|
|
reply_to = poobrains.storage.fields.ForeignKeyField(Comment, null=True)
|
|
created = poobrains.storage.fields.DateTimeField(default=datetime.datetime.now, null=False)
|
|
author = poobrains.storage.fields.CharField()
|
|
text = poobrains.storage.fields.TextField()
|
|
|
|
|
|
def view(self, mode=None, handle=None):
|
|
|
|
"""
|
|
view function to be called in a flask request context
|
|
"""
|
|
|
|
if mode == 'raw':
|
|
|
|
colors = [
|
|
(0,128,255),
|
|
(0,255,128),
|
|
(128,0,255),
|
|
(128,255,0),
|
|
(255,0,128),
|
|
(255,128,0)
|
|
]
|
|
|
|
font_path = os.path.join(app.poobrain_path, 'themes/default/fonts/knewave/knewave-outline.otf')
|
|
image = Image.new('RGBA', (250, 80), (255,255,255,0))
|
|
font = ImageFont.truetype(font_path, 42)
|
|
|
|
|
|
#x_jitter = ((image.width/10) * -1, 0)
|
|
#y_jitter = ((image.height/10) * -1, image.height/10)
|
|
x_jitter = (-5, 5)
|
|
y_jitter = (-5, 5)
|
|
|
|
textsize = font.getsize(' '.join(self.captcha))
|
|
centered = (image.width / 2 - textsize[0] / 2, image.height / 2 - textsize[1] / 2)
|
|
|
|
x = centered[0] + random.randint(x_jitter[0], x_jitter[1])
|
|
y = centered[1] + random.randint(y_jitter[0], y_jitter[1])
|
|
baseline = centered[1]
|
|
|
|
for char in self.captcha:
|
|
|
|
c = colors[random.randint(0, len(colors) -1)]
|
|
c = tuple(list(c) + [random.randint(255,255)])
|
|
|
|
|
|
char_size = font.getsize(char)
|
|
|
|
char_wrapped = ' %s ' % char
|
|
char_wrapped_size = font.getsize(char_wrapped)
|
|
|
|
char_layer = Image.new('RGBA', char_wrapped_size, (0,0,0,0))
|
|
char_draw = ImageDraw.Draw(char_layer)
|
|
|
|
char_draw.text((0,0), char_wrapped, c, font=font)
|
|
char_layer = char_layer.rotate(random.randint(-15, 15), expand=True, resample=Image.BICUBIC)
|
|
|
|
image.paste(
|
|
char_layer,
|
|
(int(x), int(y)),
|
|
mask=char_layer
|
|
)
|
|
|
|
x += char_size[0] + random.randint(x_jitter[0], x_jitter[1])
|
|
y = baseline + random.randint(y_jitter[0], y_jitter[1])
|
|
|
|
shine = image.filter(ImageFilter.GaussianBlur(radius=8))
|
|
image = Image.alpha_composite(image, shine)
|
|
|
|
out = io.BytesIO()
|
|
image.save(out, format='PNG')
|
|
|
|
return poobrains.Response(
|
|
out.getvalue(),
|
|
mimetype='image/png'
|
|
)
|
|
|
|
return ChallengeForm(self).view('full')
|
|
|
|
app.site.add_view(Challenge, '/comment/challenge/<handle>/', mode='full')
|
|
app.site.add_view(Challenge, '/comment/challenge/<handle>/raw', mode='raw')
|
|
|
|
|
|
class ChallengeForm(poobrains.form.Form):
|
|
|
|
challenge = None
|
|
response = poobrains.form.fields.Text()
|
|
submit = poobrains.form.Button('submit', label='Send')
|
|
|
|
def __init__(self, challenge):
|
|
|
|
super(ChallengeForm, self).__init__()
|
|
self.challenge = challenge
|
|
|
|
|
|
def validate(self, submit):
|
|
|
|
if not self.fields['response'].value == self.challenge.captcha:
|
|
self.challenge.captcha = self.challenge.__class__.captcha.default()
|
|
self.challenge.save()
|
|
raise poobrains.errors.ValidationError("A robot could do this better and cheaper than you.")
|
|
|
|
|
|
def process(self, submit):
|
|
|
|
try:
|
|
|
|
cls = Commentable.class_children_keyed()[self.challenge.model]
|
|
instance = cls.load(self.challenge.handle)
|
|
|
|
except KeyError:
|
|
flask.flash(u"WORNG!1!!", 'error')
|
|
app.logger.error("Challenge %s refers to non-existant model!" % self.challenge.name)
|
|
return flask.redirect('/')
|
|
|
|
except peewee.DoesNotExist:
|
|
flask.flash(u"The thing you wanted to comment on does not exist anymore.")
|
|
return flask.redirect(cls.url('teaser'))
|
|
|
|
comment = Comment()
|
|
comment.model = self.challenge.model
|
|
comment.handle = self.challenge.handle
|
|
comment.reply_to = self.challenge.reply_to
|
|
comment.author = self.challenge.author
|
|
comment.text = self.challenge.text
|
|
|
|
if comment.save():
|
|
flask.flash(u"Your comment has been saved.")
|
|
|
|
if instance.notify_owner:
|
|
instance.owner.notify("New comment on [%s/%s] by %s." % (self.challenge.model, self.challenge.handle, self.challenge.author))
|
|
|
|
self.challenge.delete_instance() # commit glorious seppuku
|
|
return flask.redirect(instance.url('full'))
|
|
|
|
flask.flash(u"Your comment could not be saved.", 'error')
|
|
|
|
|
|
@app.cron
|
|
def bury_orphaned_challenges():
|
|
|
|
deathwall = datetime.datetime.now() - datetime.timedelta(seconds=app.config['TOKEN_VALIDITY'])
|
|
|
|
q = Challenge.delete().where(Challenge.created <= deathwall)
|
|
|
|
count = q.execute()
|
|
|
|
app.logger.info("Deleted %d orphaned comment challenges." % count)
|