2017-03-25 03:47:57 +00:00
import os
2017-03-25 02:20:20 +00:00
import random
2017-03-25 03:47:57 +00:00
import functools
2017-01-23 07:19:01 +00:00
import collections
import datetime
import peewee
import flask
2017-03-25 03:47:57 +00:00
2017-05-25 06:33:33 +00:00
import io
from PIL import Image , ImageDraw , ImageFont , ImageFilter
2017-03-25 03:47:57 +00:00
2017-08-07 21:09:44 +00:00
#import poobrains
from poobrains import app
import poobrains . helpers
import poobrains . rendering
import poobrains . storage
import poobrains . form
import poobrains . auth
import poobrains . tagging
2017-01-22 22:31:29 +00:00
class Comment ( poobrains . auth . Administerable ) :
class Meta :
abstract = False
order_by = [ ' created ' ]
model = poobrains . storage . fields . CharField ( )
handle = poobrains . storage . fields . CharField ( )
2017-01-23 07:19:01 +00:00
reply_to = poobrains . storage . fields . ForeignKeyField ( ' self ' , null = True )
created = poobrains . storage . fields . DateTimeField ( default = datetime . datetime . now , null = False )
2017-01-22 22:31:29 +00:00
author = poobrains . storage . fields . CharField ( )
text = poobrains . storage . fields . TextField ( )
2018-11-07 03:22:41 +00:00
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 )
2017-01-22 22:31:29 +00:00
2017-01-23 07:19:01 +00:00
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 ) :
2017-03-26 13:55:56 +00:00
2017-06-07 20:14:04 +00:00
children = Commentable . class_children_keyed ( )
2018-10-22 16:48:23 +00:00
if self . model in children :
2017-03-26 13:55:56 +00:00
2017-06-07 20:14:04 +00:00
model = Commentable . class_children_keyed ( ) [ self . model ]
2017-08-04 05:01:29 +00:00
comments_enabled = model . load ( self . handle ) . comments_enabled
2017-04-14 19:42:28 +00:00
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
2017-03-26 13:55:56 +00:00
2017-04-14 19:42:28 +00:00
return poobrains . rendering . RenderString ( " Commenting is disabled. " )
2017-01-23 07:19:01 +00:00
2017-04-14 19:42:28 +00:00
raise Exception ( " Bork " )
2017-01-23 07:19:01 +00:00
2018-03-20 22:28:34 +00:00
@poobrains.auth.User.on_profile
2017-01-22 22:31:29 +00:00
class Commentable ( poobrains . tagging . Taggable ) :
2017-01-23 07:19:01 +00:00
class Meta :
abstract = True
2018-03-18 17:17:28 +00:00
order_by = [ ' -date ' ]
2017-01-23 07:19:01 +00:00
2018-03-04 01:03:47 +00:00
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 ' )
2018-03-18 17:17:28 +00:00
date = poobrains . storage . fields . DateTimeField ( default = datetime . datetime . now , verbose_name = ' Date ' , help_text = ' Date of publication, current time if left empty ' )
2017-01-22 22:31:29 +00:00
2018-04-05 22:27:31 +00:00
@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 '
2017-01-22 22:31:29 +00:00
2018-01-29 02:09:41 +00:00
@property
def comments ( self ) :
return Comment . select ( ) . where ( Comment . model == self . __class__ . __name__ , Comment . handle == self . handle_string )
2017-01-22 22:31:29 +00:00
2018-01-29 02:09:41 +00:00
@property
def comments_threaded ( self ) :
comments_threaded = collections . OrderedDict ( )
2017-01-22 22:31:29 +00:00
2017-03-21 05:58:54 +00:00
try :
Comment . permissions [ ' read ' ] . check ( flask . g . user )
2018-01-29 02:09:41 +00:00
for comment in self . comments . where ( Comment . reply_to == None ) : # iterate through root comments
comments_threaded [ comment ] = comment . thread ( )
2017-03-21 05:58:54 +00:00
2017-01-23 07:19:01 +00:00
2018-01-29 02:09:41 +00:00
except poobrains . auth . AccessDenied :
2018-02-25 20:37:34 +00:00
pass # No point loading shit this user isn't allowed to render anyways.
return comments_threaded
2017-07-08 23:57:58 +00:00
2017-01-23 07:19:01 +00:00
def comment_form ( self , reply_to = None ) :
2017-04-14 19:42:28 +00:00
if self . comments_enabled :
2017-03-21 05:58:54 +00:00
2017-04-14 19:42:28 +00:00
try :
Comment . permissions [ ' create ' ] . check ( flask . g . user ) # no form for users who aren't allowed to comment
2017-10-20 23:25:12 +00:00
return CommentForm ( instance = self , reply_to = reply_to )
2017-04-14 19:42:28 +00:00
except poobrains . auth . AccessDenied :
return poobrains . rendering . RenderString ( " You are not allowed to post comments. " )
return poobrains . rendering . RenderString ( " Commenting is disabled. " )
2017-03-21 05:58:54 +00:00
2017-01-23 07:19:01 +00:00
class CommentForm ( poobrains . form . Form ) :
instance = None # Commentable instance the comment is going to be associated to
reply_to = poobrains . form . fields . Value ( )
2018-01-18 00:57:01 +00:00
author = poobrains . form . fields . Text ( required = True , placeholder = ' Yer name ' )
text = poobrains . form . fields . TextArea ( required = True , placeholder = ' Yer mutterings ' )
2017-01-23 07:19:01 +00:00
submit = poobrains . form . Button ( ' submit ' , label = ' Send comment ' )
2019-03-14 15:32:21 +00:00
def __init__ ( self , model = None , instance_handle = None , instance = None , * * kwargs ) :
2017-10-20 23:25:12 +00:00
if not isinstance ( instance , Commentable ) :
2019-03-14 15:32:21 +00:00
assert model and instance_handle , " Either instance (a Commentable instance) or model AND handle must be passed. "
2017-10-20 23:25:12 +00:00
2017-10-22 17:04:48 +00:00
cls = Commentable . class_children_keyed ( ) [ model ]
2019-03-14 15:32:21 +00:00
instance = cls . load ( instance_handle )
2017-02-07 04:53:24 +00:00
2018-10-22 16:48:23 +00:00
reply_to = kwargs . pop ( ' reply_to ' ) if ' reply_to ' in kwargs else None
2017-01-23 21:44:16 +00:00
if isinstance ( reply_to , int ) :
reply_to = Comment . load ( reply_to )
2017-01-23 07:19:01 +00:00
super ( CommentForm , self ) . __init__ ( * * kwargs )
self . instance = instance
2017-02-07 17:59:42 +00:00
self . fields [ ' reply_to ' ] . value = reply_to
2017-01-23 07:19:01 +00:00
2021-05-14 19:03:36 +00:00
self . action = f " /comment/ { 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?
2017-01-23 21:44:16 +00:00
if reply_to :
2021-05-14 19:03:36 +00:00
self . action + = f " / { reply_to . id } "
2017-01-23 07:19:01 +00:00
2017-09-30 02:21:33 +00:00
def process ( self , submit ) :
2017-01-23 07:19:01 +00:00
2017-01-23 21:44:16 +00:00
self . instance . permissions [ ' read ' ] . check ( flask . g . user )
2017-03-25 02:20:20 +00:00
iteration_limit = 10
for i in range ( 0 , iteration_limit ) :
2017-03-25 03:47:57 +00:00
name = poobrains . helpers . random_string_light ( 16 ) . lower ( )
if not Challenge . select ( ) . where ( Challenge . name == name ) . count ( ) :
2017-03-25 02:20:20 +00:00
break
2017-03-25 03:47:57 +00:00
elif i == iteration_limit - 1 : # means loop ran through without finding a free challenge name
2017-06-10 23:47:12 +00:00
flask . flash ( u " I ' m sorry Dave. I ' m afraid I can ' t do that. " )
2017-03-25 02:20:20 +00:00
return flask . redirect ( self . instance . url ( ' full ' ) )
2017-03-25 03:47:57 +00:00
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
2017-03-25 02:20:20 +00:00
2017-03-25 03:47:57 +00:00
challenge . save ( )
2017-04-07 07:35:04 +00:00
2017-03-25 03:47:57 +00:00
return flask . redirect ( challenge . url ( ' full ' ) )
2017-03-25 02:20:20 +00:00
2019-03-14 15:32:21 +00:00
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 ' )
2017-03-25 02:20:20 +00:00
2019-03-14 15:32:21 +00:00
class Challenge ( poobrains . storage . Named ) :
2017-03-25 02:20:20 +00:00
2017-03-25 03:47:57 +00:00
class Meta :
2017-04-10 01:28:29 +00:00
modes = collections . OrderedDict ( [ ( ' full ' , ' read ' ) , ( ' raw ' , ' read ' ) ] )
2017-03-25 02:20:20 +00:00
2017-03-25 03:47:57 +00:00
title = ' Fuck bots, get bugs '
captcha = poobrains . storage . fields . CharField ( default = functools . partial ( poobrains . helpers . random_string_light , 6 ) )
2017-03-25 02:20:20 +00:00
model = poobrains . storage . fields . CharField ( )
handle = poobrains . storage . fields . CharField ( )
2017-04-13 18:25:04 +00:00
reply_to = poobrains . storage . fields . ForeignKeyField ( Comment , null = True )
2017-03-25 02:20:20 +00:00
created = poobrains . storage . fields . DateTimeField ( default = datetime . datetime . now , null = False )
author = poobrains . storage . fields . CharField ( )
text = poobrains . storage . fields . TextField ( )
2017-01-23 21:44:16 +00:00
2017-03-25 03:47:57 +00:00
2017-03-25 02:20:20 +00:00
def view ( self , mode = None , handle = None ) :
"""
view function to be called in a flask request context
"""
2017-01-23 07:19:01 +00:00
2017-03-25 03:47:57 +00:00
if mode == ' raw ' :
2017-01-23 07:19:01 +00:00
2017-03-25 03:47:57 +00:00
colors = [
2017-05-25 06:33:33 +00:00
( 0 , 128 , 255 ) ,
( 0 , 255 , 128 ) ,
( 128 , 0 , 255 ) ,
( 128 , 255 , 0 ) ,
( 255 , 0 , 128 ) ,
( 255 , 128 , 0 )
2017-03-25 03:47:57 +00:00
]
2017-03-25 02:20:20 +00:00
2017-08-07 21:09:44 +00:00
font_path = os . path . join ( app . poobrain_path , ' themes/default/fonts/knewave/knewave-outline.otf ' )
2017-05-25 06:33:33 +00:00
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)
2017-03-25 03:47:57 +00:00
x_jitter = ( - 5 , 5 )
2017-05-25 06:33:33 +00:00
y_jitter = ( - 5 , 5 )
textsize = font . getsize ( ' ' . join ( self . captcha ) )
centered = ( image . width / 2 - textsize [ 0 ] / 2 , image . height / 2 - textsize [ 1 ] / 2 )
2017-03-25 02:20:20 +00:00
2017-05-25 06:33:33 +00:00
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 ]
2017-03-25 02:20:20 +00:00
2017-03-25 03:47:57 +00:00
for char in self . captcha :
2017-03-25 02:20:20 +00:00
2017-03-25 03:47:57 +00:00
c = colors [ random . randint ( 0 , len ( colors ) - 1 ) ]
2017-05-25 06:33:33 +00:00
c = tuple ( list ( c ) + [ random . randint ( 255 , 255 ) ] )
char_size = font . getsize ( char )
2021-05-14 19:03:36 +00:00
char_wrapped = f ' { char } '
2017-05-25 06:33:33 +00:00
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 ,
2018-11-07 03:22:41 +00:00
( int ( x ) , int ( y ) ) ,
mask = char_layer
2017-05-25 06:33:33 +00:00
)
x + = char_size [ 0 ] + random . randint ( x_jitter [ 0 ] , x_jitter [ 1 ] )
2017-03-25 03:47:57 +00:00
y = baseline + random . randint ( y_jitter [ 0 ] , y_jitter [ 1 ] )
2017-05-25 06:33:33 +00:00
shine = image . filter ( ImageFilter . GaussianBlur ( radius = 8 ) )
image = Image . alpha_composite ( image , shine )
out = io . BytesIO ( )
image . save ( out , format = ' PNG ' )
2017-03-25 02:20:20 +00:00
2020-06-16 15:36:33 +00:00
return poobrains . Response (
2017-05-25 06:33:33 +00:00
out . getvalue ( ) ,
2017-03-25 03:47:57 +00:00
mimetype = ' image/png '
)
return ChallengeForm ( self ) . view ( ' full ' )
2017-08-07 21:09:44 +00:00
app . site . add_view ( Challenge , ' /comment/challenge/<handle>/ ' , mode = ' full ' )
app . site . add_view ( Challenge , ' /comment/challenge/<handle>/raw ' , mode = ' raw ' )
2017-03-25 03:47:57 +00:00
class ChallengeForm ( poobrains . form . Form ) :
challenge = None
response = poobrains . form . fields . Text ( )
2017-03-25 13:52:11 +00:00
submit = poobrains . form . Button ( ' submit ' , label = ' Send ' )
2017-03-25 03:47:57 +00:00
def __init__ ( self , challenge ) :
2017-01-23 07:19:01 +00:00
2017-03-25 13:52:11 +00:00
super ( ChallengeForm , self ) . __init__ ( )
2017-03-25 03:47:57 +00:00
self . challenge = challenge
2017-11-06 02:44:32 +00:00
2017-03-25 13:52:11 +00:00
2020-05-31 23:56:38 +00:00
def validate ( self , submit ) :
2017-11-06 02:44:32 +00:00
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. " )
2017-03-25 13:52:11 +00:00
2017-11-06 02:44:32 +00:00
def process ( self , submit ) :
2017-03-25 13:52:11 +00:00
2017-11-06 02:44:32 +00:00
try :
2017-03-25 13:52:11 +00:00
2017-11-06 02:44:32 +00:00
cls = Commentable . class_children_keyed ( ) [ self . challenge . model ]
instance = cls . load ( self . challenge . handle )
2017-03-25 13:52:11 +00:00
2017-11-06 02:44:32 +00:00
except KeyError :
2019-03-16 08:52:54 +00:00
flask . flash ( u " WORNG!1!! " , ' error ' )
2021-05-14 19:03:36 +00:00
app . logger . error ( f " Challenge { self . challenge . name } refers to non-existant model! " )
2017-11-06 02:44:32 +00:00
return flask . redirect ( ' / ' )
2017-03-25 13:52:11 +00:00
2017-11-06 02:44:32 +00:00
except peewee . DoesNotExist :
2021-05-14 19:03:36 +00:00
flask . flash ( " The thing you wanted to comment on does not exist anymore. " )
2017-11-06 02:44:32 +00:00
return flask . redirect ( cls . url ( ' teaser ' ) )
2017-04-07 07:35:04 +00:00
2017-11-06 02:44:32 +00:00
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
2017-04-07 07:35:04 +00:00
2017-11-06 02:44:32 +00:00
if comment . save ( ) :
flask . flash ( u " Your comment has been saved. " )
2017-03-25 13:52:11 +00:00
2017-11-06 02:44:32 +00:00
if instance . notify_owner :
2021-05-14 19:03:36 +00:00
instance . owner . notify ( f " New comment on [ { self . challenge . model } / { self . challenge . handle } ] by { self . challenge . author } . " )
2017-11-06 02:44:32 +00:00
self . challenge . delete_instance ( ) # commit glorious seppuku
return flask . redirect ( instance . url ( ' full ' ) )
2017-03-25 13:52:11 +00:00
2017-11-06 02:44:32 +00:00
flask . flash ( u " Your comment could not be saved. " , ' error ' )
2017-07-08 16:09:56 +00:00
2017-08-07 21:09:44 +00:00
@app.cron
2017-07-08 16:09:56 +00:00
def bury_orphaned_challenges ( ) :
2017-08-07 21:09:44 +00:00
deathwall = datetime . datetime . now ( ) - datetime . timedelta ( seconds = app . config [ ' TOKEN_VALIDITY ' ] )
2017-07-08 16:09:56 +00:00
q = Challenge . delete ( ) . where ( Challenge . created < = deathwall )
count = q . execute ( )
2021-05-14 19:03:36 +00:00
app . logger . info ( f " Deleted { count } orphaned comment challenges. " )