2024-02-15 09:17:13 -08:00
from flask import request , g
2024-01-24 23:16:08 -08:00
from flask_login import current_user
2023-08-29 03:01:06 -07:00
from flask_wtf import FlaskForm
2024-03-14 18:24:45 -07:00
from wtforms import StringField , SubmitField , TextAreaField , BooleanField , HiddenField , SelectField , FileField , \
DateField
2024-05-08 02:07:22 -07:00
from wtforms . fields . choices import SelectMultipleField
2024-03-26 09:29:08 -07:00
from wtforms . validators import ValidationError , DataRequired , Email , EqualTo , Length , Regexp , Optional
2023-08-29 03:01:06 -07:00
from flask_babel import _ , lazy_gettext as _l
2024-01-24 23:16:08 -08:00
from app import db
2025-01-10 15:07:58 -08:00
from app . constants import DOWNVOTE_ACCEPT_ALL , DOWNVOTE_ACCEPT_MEMBERS , DOWNVOTE_ACCEPT_INSTANCE , \
DOWNVOTE_ACCEPT_TRUSTED
2024-03-14 18:24:45 -07:00
from app . models import Community , utcnow
2023-12-13 00:04:11 -08:00
from app . utils import domain_from_url , MultiCheckboxField
2025-01-17 14:47:40 -08:00
from PIL import Image , ImageOps , UnidentifiedImageError
2024-01-24 23:16:08 -08:00
from io import BytesIO
import pytesseract
2023-09-17 02:19:51 -07:00
2023-08-29 03:01:06 -07:00
2024-03-12 20:40:20 -07:00
class AddCommunityForm ( FlaskForm ) :
2023-09-02 21:30:20 -07:00
community_name = StringField ( _l ( ' Name ' ) , validators = [ DataRequired ( ) ] )
2023-12-07 20:13:38 -08:00
url = StringField ( _l ( ' Url ' ) )
2023-08-29 03:01:06 -07:00
description = TextAreaField ( _l ( ' Description ' ) )
2024-06-15 23:03:59 -07:00
icon_file = FileField ( _l ( ' Icon image ' ) , render_kw = { ' accept ' : ' image/* ' } )
banner_file = FileField ( _l ( ' Banner image ' ) , render_kw = { ' accept ' : ' image/* ' } )
2023-08-29 03:01:06 -07:00
rules = TextAreaField ( _l ( ' Rules ' ) )
2023-12-30 15:09:20 -08:00
nsfw = BooleanField ( ' NSFW ' )
2024-01-26 14:19:23 -08:00
local_only = BooleanField ( ' Local only ' )
2024-05-08 02:07:22 -07:00
languages = SelectMultipleField ( _l ( ' Languages ' ) , coerce = int , validators = [ Optional ( ) ] , render_kw = { ' class ' : ' form-select ' } )
2023-08-29 03:01:06 -07:00
submit = SubmitField ( _l ( ' Create ' ) )
2023-11-16 01:31:14 -08:00
def validate ( self , extra_validators = None ) :
if not super ( ) . validate ( ) :
return False
if self . url . data . strip ( ) == ' ' :
2024-04-30 02:29:49 -07:00
self . url . errors . append ( _l ( ' Url is required. ' ) )
2023-11-16 01:31:14 -08:00
return False
else :
if ' - ' in self . url . data . strip ( ) :
2024-04-30 02:29:49 -07:00
self . url . errors . append ( _l ( ' - cannot be in Url. Use _ instead? ' ) )
return False
community = Community . query . filter ( Community . name == self . url . data . strip ( ) . lower ( ) , Community . ap_id == None ) . first ( )
if community is not None :
self . url . errors . append ( _l ( ' A community with this url already exists. ' ) )
2023-11-16 01:31:14 -08:00
return False
return True
2023-08-29 03:01:06 -07:00
2024-03-12 20:40:20 -07:00
class EditCommunityForm ( FlaskForm ) :
title = StringField ( _l ( ' Title ' ) , validators = [ DataRequired ( ) ] )
description = TextAreaField ( _l ( ' Description ' ) )
2024-06-15 23:03:59 -07:00
icon_file = FileField ( _l ( ' Icon image ' ) , render_kw = { ' accept ' : ' image/* ' } )
banner_file = FileField ( _l ( ' Banner image ' ) , render_kw = { ' accept ' : ' image/* ' } )
2024-03-12 20:40:20 -07:00
rules = TextAreaField ( _l ( ' Rules ' ) )
nsfw = BooleanField ( _l ( ' Porn community ' ) )
local_only = BooleanField ( _l ( ' Only accept posts from current instance ' ) )
restricted_to_mods = BooleanField ( _l ( ' Only moderators can post ' ) )
new_mods_wanted = BooleanField ( _l ( ' New moderators wanted ' ) )
2025-01-10 15:07:58 -08:00
downvote_accept_modes = [ ( DOWNVOTE_ACCEPT_ALL , _l ( ' Everyone ' ) ) ,
( DOWNVOTE_ACCEPT_MEMBERS , _l ( ' Community members ' ) ) ,
( DOWNVOTE_ACCEPT_INSTANCE , _l ( ' This instance ' ) ) ,
( DOWNVOTE_ACCEPT_TRUSTED , _l ( ' Trusted instances ' ) ) ,
]
downvote_accept_mode = SelectField ( _l ( ' Accept downvotes from ' ) , coerce = int , choices = downvote_accept_modes , validators = [ Optional ( ) ] , render_kw = { ' class ' : ' form-select ' } )
2024-05-17 02:27:45 -07:00
topic = SelectField ( _l ( ' Topic ' ) , coerce = int , validators = [ Optional ( ) ] , render_kw = { ' class ' : ' form-select ' } )
2024-05-08 02:07:22 -07:00
languages = SelectMultipleField ( _l ( ' Languages ' ) , coerce = int , validators = [ Optional ( ) ] , render_kw = { ' class ' : ' form-select ' } )
2024-03-12 20:40:20 -07:00
layouts = [ ( ' ' , _l ( ' List ' ) ) ,
( ' masonry ' , _l ( ' Masonry ' ) ) ,
( ' masonry_wide ' , _l ( ' Wide masonry ' ) ) ]
2024-05-17 02:27:45 -07:00
default_layout = SelectField ( _l ( ' Layout ' ) , coerce = str , choices = layouts , validators = [ Optional ( ) ] , render_kw = { ' class ' : ' form-select ' } )
2024-03-12 20:40:20 -07:00
submit = SubmitField ( _l ( ' Save ' ) )
2024-07-17 07:11:31 -07:00
class EditCommunityWikiPageForm ( FlaskForm ) :
title = StringField ( _l ( ' Title ' ) , validators = [ DataRequired ( ) ] )
slug = StringField ( _l ( ' Slug ' ) , validators = [ DataRequired ( ) ] )
body = TextAreaField ( _l ( ' Body ' ) , render_kw = { ' rows ' : ' 10 ' } )
edit_options = [ ( 0 , _l ( ' Mods and admins ' ) ) ,
( 1 , _l ( ' Trusted accounts ' ) ) ,
( 2 , _l ( ' Community members ' ) ) ,
( 3 , _l ( ' Any account ' ) )
]
who_can_edit = SelectField ( _l ( ' Who can edit ' ) , coerce = int , choices = edit_options , validators = [ Optional ( ) ] , render_kw = { ' class ' : ' form-select ' } )
submit = SubmitField ( _l ( ' Save ' ) )
2024-03-12 20:40:20 -07:00
class AddModeratorForm ( FlaskForm ) :
user_name = StringField ( _l ( ' User name ' ) , validators = [ DataRequired ( ) ] )
2024-07-13 23:01:22 -07:00
submit = SubmitField ( _l ( ' Find ' ) )
2024-03-12 20:40:20 -07:00
2024-03-27 00:20:08 -07:00
class EscalateReportForm ( FlaskForm ) :
reason = StringField ( _l ( ' Amend the report description if necessary ' ) , validators = [ DataRequired ( ) ] )
submit = SubmitField ( _l ( ' Escalate report ' ) )
class ResolveReportForm ( FlaskForm ) :
note = StringField ( _l ( ' Note for mod log ' ) , validators = [ Optional ( ) ] )
also_resolve_others = BooleanField ( _l ( ' Also resolve all other reports about the same thing. ' ) , default = True )
submit = SubmitField ( _l ( ' Resolve report ' ) )
2023-08-29 03:01:06 -07:00
class SearchRemoteCommunity ( FlaskForm ) :
2024-01-29 01:18:06 -08:00
address = StringField ( _l ( ' Community address ' ) , render_kw = { ' placeholder ' : ' e.g. !name@server ' , ' autofocus ' : True } , validators = [ DataRequired ( ) ] )
2023-09-17 02:19:51 -07:00
submit = SubmitField ( _l ( ' Search ' ) )
2024-03-14 18:24:45 -07:00
class BanUserCommunityForm ( FlaskForm ) :
reason = StringField ( _l ( ' Reason ' ) , render_kw = { ' autofocus ' : True } , validators = [ DataRequired ( ) ] )
2024-03-21 01:19:50 -07:00
ban_until = DateField ( _l ( ' Ban until ' ) , validators = [ Optional ( ) ] )
2024-03-14 18:24:45 -07:00
delete_posts = BooleanField ( _l ( ' Also delete all their posts ' ) )
delete_post_replies = BooleanField ( _l ( ' Also delete all their comments ' ) )
submit = SubmitField ( _l ( ' Ban ' ) )
2024-07-08 04:21:02 -07:00
class CreatePostForm ( FlaskForm ) :
2024-05-11 18:02:45 -07:00
communities = SelectField ( _l ( ' Community ' ) , validators = [ DataRequired ( ) ] , coerce = int , render_kw = { ' class ' : ' form-select ' } )
2024-07-08 04:21:02 -07:00
title = StringField ( _l ( ' Title ' ) , validators = [ DataRequired ( ) , Length ( min = 3 , max = 255 ) ] )
body = TextAreaField ( _l ( ' Body ' ) , validators = [ Optional ( ) , Length ( min = 3 , max = 5000 ) ] , render_kw = { ' rows ' : 5 } )
2024-05-11 18:02:45 -07:00
tags = StringField ( _l ( ' Tags ' ) , validators = [ Optional ( ) , Length ( min = 3 , max = 5000 ) ] )
2024-04-06 16:15:04 -07:00
sticky = BooleanField ( _l ( ' Sticky ' ) )
nsfw = BooleanField ( _l ( ' NSFW ' ) )
nsfl = BooleanField ( _l ( ' Gore/gross ' ) )
notify_author = BooleanField ( _l ( ' Notify about replies ' ) )
2024-05-08 22:54:30 -07:00
language_id = SelectField ( _l ( ' Language ' ) , validators = [ DataRequired ( ) ] , coerce = int , render_kw = { ' class ' : ' form-select ' } )
2024-04-06 16:15:04 -07:00
submit = SubmitField ( _l ( ' Save ' ) )
2024-07-08 04:21:02 -07:00
class CreateDiscussionForm ( CreatePostForm ) :
pass
class CreateLinkForm ( CreatePostForm ) :
2024-04-06 16:15:04 -07:00
link_url = StringField ( _l ( ' URL ' ) , validators = [ DataRequired ( ) , Regexp ( r ' ^https?:// ' , message = ' Submitted links need to start with " http:// " " or " https:// " ' ) ] ,
2024-08-15 00:39:33 -07:00
render_kw = { ' placeholder ' : ' https://... ' ,
' hx-get ' : ' /community/check_url_already_posted ' ,
' hx-params ' : ' * ' ,
' hx-target ' : ' #urlUsed ' } )
2023-09-17 02:19:51 -07:00
def validate ( self , extra_validators = None ) - > bool :
2024-04-06 16:15:04 -07:00
domain = domain_from_url ( self . link_url . data , create = False )
if domain and domain . banned :
2024-04-30 02:29:49 -07:00
self . link_url . errors . append ( _l ( " Links to %(domain)s are not allowed. " , domain = domain . name ) )
2023-09-17 02:19:51 -07:00
return False
2024-04-06 16:15:04 -07:00
return True
2023-09-17 02:19:51 -07:00
2024-04-06 16:15:04 -07:00
2024-07-08 04:21:02 -07:00
class CreateVideoForm ( CreatePostForm ) :
2024-04-16 01:59:58 -07:00
video_url = StringField ( _l ( ' URL ' ) , validators = [ DataRequired ( ) , Regexp ( r ' ^https?:// ' , message = ' Submitted links need to start with " http:// " " or " https:// " ' ) ] ,
render_kw = { ' placeholder ' : ' https://... ' } )
def validate ( self , extra_validators = None ) - > bool :
domain = domain_from_url ( self . video_url . data , create = False )
if domain and domain . banned :
2024-04-30 02:29:49 -07:00
self . video_url . errors . append ( _l ( " Videos from %(domain)s are not allowed. " , domain = domain . name ) )
2024-04-16 01:59:58 -07:00
return False
return True
2024-07-08 04:21:02 -07:00
class CreateImageForm ( CreatePostForm ) :
2024-05-25 03:44:43 -07:00
image_alt_text = StringField ( _l ( ' Alt text ' ) , validators = [ Optional ( ) , Length ( min = 3 , max = 1500 ) ] )
2024-06-15 23:03:59 -07:00
image_file = FileField ( _l ( ' Image ' ) , validators = [ DataRequired ( ) ] , render_kw = { ' accept ' : ' image/* ' } )
2024-04-06 16:15:04 -07:00
def validate ( self , extra_validators = None ) - > bool :
uploaded_file = request . files [ ' image_file ' ]
2024-12-05 13:21:44 -08:00
if uploaded_file and uploaded_file . filename != ' ' and not uploaded_file . filename . endswith ( ' .svg ' ) :
2024-04-06 16:15:04 -07:00
Image . MAX_IMAGE_PIXELS = 89478485
# Do not allow fascist meme content
try :
2024-12-05 12:44:56 -08:00
if ' .avif ' in uploaded_file . filename :
import pillow_avif
2024-04-06 16:15:04 -07:00
image_text = pytesseract . image_to_string ( Image . open ( BytesIO ( uploaded_file . read ( ) ) ) . convert ( ' L ' ) )
except FileNotFoundError as e :
image_text = ' '
2025-01-17 14:47:40 -08:00
except UnidentifiedImageError as e :
image_text = ' '
2024-04-06 16:15:04 -07:00
if ' Anonymous ' in image_text and (
' No. ' in image_text or ' N0 ' in image_text ) : # chan posts usually contain the text 'Anonymous' and ' No.12345'
self . image_file . errors . append (
f " This image is an invalid file type. " ) # deliberately misleading error message
current_user . reputation - = 1
db . session . commit ( )
2023-09-17 02:19:51 -07:00
return False
2024-04-06 16:15:04 -07:00
if self . communities :
community = Community . query . get ( self . communities . data )
if community . is_local ( ) and g . site . allow_local_image_posts is False :
2024-04-30 02:29:49 -07:00
self . communities . errors . append ( _l ( ' Images cannot be posted to local communities. ' ) )
2023-09-17 02:19:51 -07:00
return True
2023-10-02 02:16:44 -07:00
2024-12-14 14:30:45 -08:00
class EditImageForm ( CreateImageForm ) :
image_file = FileField ( _l ( ' Replace Image ' ) , validators = [ DataRequired ( ) ] , render_kw = { ' accept ' : ' image/* ' } )
2024-12-21 04:05:14 -08:00
image_file = FileField ( _l ( ' Image ' ) , validators = [ Optional ( ) ] , render_kw = { ' accept ' : ' image/* ' } )
2024-12-14 14:30:45 -08:00
2024-08-16 13:35:16 -07:00
def validate ( self , extra_validators = None ) - > bool :
if self . communities :
community = Community . query . get ( self . communities . data )
if community . is_local ( ) and g . site . allow_local_image_posts is False :
self . communities . errors . append ( _l ( ' Images cannot be posted to local communities. ' ) )
return True
2023-12-13 00:04:11 -08:00
2024-07-08 04:21:02 -07:00
class CreatePollForm ( CreatePostForm ) :
2024-05-18 01:17:05 -07:00
mode = SelectField ( _ ( ' Mode ' ) , validators = [ DataRequired ( ) ] , choices = [ ( ' single ' , _l ( ' Voters choose one option ' ) ) , ( ' multiple ' , _l ( ' Voters choose many options ' ) ) ] , render_kw = { ' class ' : ' form-select ' } )
2024-05-16 02:53:38 -07:00
finish_choices = [
( ' 30m ' , _l ( ' 30 minutes ' ) ) ,
( ' 1h ' , _l ( ' 1 hour ' ) ) ,
( ' 6h ' , _l ( ' 6 hours ' ) ) ,
( ' 12h ' , _l ( ' 12 hours ' ) ) ,
( ' 1d ' , _l ( ' 1 day ' ) ) ,
( ' 3d ' , _l ( ' 3 days ' ) ) ,
( ' 7d ' , _l ( ' 7 days ' ) ) ,
]
2024-05-17 02:27:45 -07:00
finish_in = SelectField ( _ ( ' End voting in ' ) , validators = [ DataRequired ( ) ] , choices = finish_choices , render_kw = { ' class ' : ' form-select ' } )
2024-05-16 02:53:38 -07:00
local_only = BooleanField ( _l ( ' Accept votes from this instance only ' ) )
choice_1 = StringField ( ' Choice ' ) # intentionally left out of internationalization (no _l()) as this label is not used
choice_2 = StringField ( ' Choice ' )
choice_3 = StringField ( ' Choice ' )
choice_4 = StringField ( ' Choice ' )
choice_5 = StringField ( ' Choice ' )
choice_6 = StringField ( ' Choice ' )
choice_7 = StringField ( ' Choice ' )
choice_8 = StringField ( ' Choice ' )
choice_9 = StringField ( ' Choice ' )
choice_10 = StringField ( ' Choice ' )
2024-05-18 00:41:20 -07:00
def validate ( self , extra_validators = None ) - > bool :
choices_made = 0
for i in range ( 1 , 10 ) :
choice_data = getattr ( self , f " choice_ { i } " ) . data . strip ( )
if choice_data != ' ' :
choices_made + = 1
if choices_made == 0 :
self . choice_1 . errors . append ( _l ( ' Polls need options for people to choose from ' ) )
return False
elif choices_made < = 1 :
self . choice_2 . errors . append ( _l ( ' Provide at least two choices ' ) )
return False
return True
2024-05-16 02:53:38 -07:00
2023-12-13 00:04:11 -08:00
class ReportCommunityForm ( FlaskForm ) :
reason_choices = [ ( ' 1 ' , _l ( ' Breaks instance rules ' ) ) ,
( ' 2 ' , _l ( ' Abandoned by moderators ' ) ) ,
( ' 3 ' , _l ( ' Cult ' ) ) ,
( ' 4 ' , _l ( ' Scam ' ) ) ,
( ' 5 ' , _l ( ' Alt-right pipeline ' ) ) ,
( ' 6 ' , _l ( ' Hate / genocide ' ) ) ,
( ' 7 ' , _l ( ' Other ' ) ) ,
]
reasons = MultiCheckboxField ( _l ( ' Reason ' ) , choices = reason_choices )
description = StringField ( _l ( ' More info ' ) )
report_remote = BooleanField ( ' Also send report to originating instance ' )
submit = SubmitField ( _l ( ' Report ' ) )
2023-12-21 01:14:43 -08:00
2024-03-11 13:53:34 -07:00
def reasons_to_string ( self , reason_data ) - > str :
result = [ ]
for reason_id in reason_data :
for choice in self . reason_choices :
if choice [ 0 ] == reason_id :
result . append ( str ( choice [ 1 ] ) )
return ' , ' . join ( result )
2023-12-21 01:14:43 -08:00
class DeleteCommunityForm ( FlaskForm ) :
submit = SubmitField ( _l ( ' Delete community ' ) )
2024-05-27 15:51:14 -07:00
class RetrieveRemotePost ( FlaskForm ) :
address = StringField ( _l ( ' Full URL ' ) , render_kw = { ' placeholder ' : ' e.g. https://lemmy.world/post/123 ' , ' autofocus ' : True } , validators = [ DataRequired ( ) ] )
submit = SubmitField ( _l ( ' Retrieve ' ) )