diff --git a/README.foreskin b/README.foreskin index 8e3bd64..0c6045c 100644 --- a/README.foreskin +++ b/README.foreskin @@ -4,7 +4,10 @@ Stuff that is done: - Hide email password reset form if no SMTP server is set up. (done by Arian Kordi) - We are now using django groups. - Admins have a better interface. -- Post limit shows publicly, with indication on how many posts are left. +- Post limit shows for admins, with indication on how many posts are left. +- Image and video file boxes in one. +- Full ImageField integration. + Stuff that is in progress - New Warning system. @@ -17,18 +20,13 @@ Todo: When banning someone, add the option to ban or revoke invites for whoever invited that person. - FIX THE FUCKING CONTACT PAGE (nah not doing that lmao) - also, CONTINUE MOVING OVER TO FORMS.PY - Profile settings Email reset form - Post forms Signup page A bunch of other forms I haven't thought about. Ideas: - An account approval system, we really need one. "is_new" exists in the user model, we just have to put that to use. - "Muting" feature to replace "active" -- Image and video file boxes in one. - Make it, so you need to enter your current password to change your email address. - Ways for mods to view who invited who easily. -- Full ImageField integration. -- Filefield may have to be used for both photos and videos. - remove the useless feedback thing. (You can just make a bug reporting community) diff --git a/README.md b/README.md index b509fba..f7c84e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # About Cedar-Django is a fork of Closedverse with custom features added to it. +### Upload anything you want. +You can upload whatever files you want to the site. The primary supported file types are Images, Videos and audio files. Other files will have a download button. + ### Theme changing Just like in Indigo, you can change the color of your theme. While this is only visible to each user specifically, a global theme can be set in Settings.py. @@ -33,6 +36,7 @@ Moderators and staff will be able to revoke a user's ability to add new users if - Django 3.2.2 - urllib3 - lxml +- django-cleanup - passlib - bcrypt - pillow @@ -59,7 +63,7 @@ You need Pip 4. Get everything else you need. -`pip3 install Django==3.2.2 urllib3 lxml passlib bcrypt pillow django-markdown-deux django-markdown2 whitenoise django-xff` +`pip3 install Django==3.2.2 urllib3 lxml passlib bcrypt pillow django-markdown-deux django-markdown2 whitenoise django-xff django-cleanup` 5. Clone the clone! diff --git a/closedverse/settings.py b/closedverse/settings.py index 40571d4..06fce43 100644 --- a/closedverse/settings.py +++ b/closedverse/settings.py @@ -29,7 +29,7 @@ DEBUG = False # Do not include 127.0.0.1 or localhost # in production for security reasons. ALLOWED_HOSTS = [ - '', + '127.0.0.1', ] DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' @@ -48,6 +48,8 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'markdown_deux', 'closedverse_main', + # Comment this out to disable file cleanup. + 'django_cleanup.apps.CleanupConfig', ] X_FRAME_OPTIONS='SAMEORIGIN' @@ -229,12 +231,11 @@ LOGIN_EXEMPT_URLS = { r'^help/rules$', r'^help/contact$', r'^help/login$', + r'^s/.*$', } -# Action to perform on images belonging to posts/ -# comments when they are deleted -# 0: keep, 1: move to 'rm' folder, 2: delete -IMAGE_DELETE_SETTING = 2 +# The max file size allowed. (default: 10*1024*1024") +max_file_size = 10*1024*1024 # allow sign ups. allow_signups = True @@ -253,14 +254,8 @@ level_needed_to_man_communities = 5 # if someone's level is equal or above this, they can edit any user with a lower level on your clone. level_needed_to_man_users = 5 -# minimum_password_length is removed as the AUTH_PASSWORD_VALIDATORS work as of 08/2023 - -# The hard limit for uploading, Will cause an error if this is exceeded. This is set to 15MB by default (15728640) -DATA_UPLOAD_MAX_MEMORY_SIZE = 52428800 - # Set the default theme here! Set this to None to use the normal Closedverse theme. -# TODO, make this work for users who aren't logged in. -site_wide_theme_hex = 'ff4159' +site_wide_theme_hex = None # age (minimal 13 due to C.O.P.P.A) age_allowed = "13" diff --git a/closedverse_main/admin.py b/closedverse_main/admin.py index 4acd4e5..a891e58 100644 --- a/closedverse_main/admin.py +++ b/closedverse_main/admin.py @@ -56,7 +56,7 @@ class UserAdmin(BaseUserAdmin): fieldsets = ( (None, {'fields': ('nickname', 'username', 'password')}), ('Personal info', {'fields': ('email', ('addr', 'signup_addr'))}), - ('Cosmetic', {'fields': (('role', 'avatar', 'has_mh'), 'color', 'theme', 'bg_url',)}), + ('Cosmetic', {'fields': (('role', 'avatar_type', 'avatar_input', 'avatar_upload'), 'color', 'theme')}), ('Data', {'fields': ('last_login', 'created', 'hide_online')}), ('Permissions', {'fields': (('is_active', 'is_staff', 'is_superuser', 'can_invite'), 'level', 'c_tokens', 'groups', 'user_permissions')}), ) @@ -132,7 +132,7 @@ class ConversationAdmin(admin.ModelAdmin): class PostAdmin(admin.ModelAdmin): raw_id_fields = ('creator', 'poll', 'community', ) search_fields = ('id', 'body', 'creator__username', ) - list_display = ('id', 'created', 'creator', 'body', 'is_rm', ) + list_display = ('id', 'created', 'creator', 'body', 'file', 'is_rm', ) list_filter = ('is_rm', 'created', ) actions = [Hide_content, Show_content, Disable_comments, Enable_comments] def get_queryset(self, request): @@ -141,7 +141,7 @@ class PostAdmin(admin.ModelAdmin): class CommentAdmin(admin.ModelAdmin): raw_id_fields = ('creator', 'original_post', 'community', ) search_fields = ('id', 'body', 'creator__username', ) - list_display = ('id', 'created', 'creator', 'body', 'original_post', 'is_rm', ) + list_display = ('id', 'created', 'creator', 'body', 'file', 'original_post', 'is_rm', ) list_filter = ('is_rm', 'created', ) actions = [Hide_content, Show_content] def get_queryset(self, request): @@ -159,7 +159,7 @@ class CommunityAdmin(admin.ModelAdmin): class MessageAdmin(admin.ModelAdmin): raw_id_fields = ('creator', 'conversation', ) search_fields = ('id', 'body', 'creator__username', ) - list_display = ('id', 'created', 'creator', 'conversation', 'body', 'read', 'is_rm', ) + list_display = ('id', 'created', 'creator', 'conversation', 'body', 'file', 'read', 'is_rm', ) list_filter = ('is_rm', 'read', 'created', ) actions = [Hide_content, Show_content] def get_queryset(self, request): diff --git a/closedverse_main/forms.py b/closedverse_main/forms.py index bde2e5b..7f6f05d 100644 --- a/closedverse_main/forms.py +++ b/closedverse_main/forms.py @@ -7,67 +7,146 @@ from django.core.files.base import ContentFile from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from django.utils.timezone import timedelta +from django.core.validators import URLValidator from closedverse import settings +from django.core.validators import EmailValidator -# will be used for community icons and profile pictures -def compress_and_resize_icon(image): - im = Image.open(image) - im = im.convert('RGB') # Convert to RGB +def compress_and_resize_content(image, icon=False): + try: + im = Image.open(image) + im = im.convert('RGB') - # Crop to 1:1 aspect ratio - width, height = im.size - min_dimension = min(width, height) - left = (width - min_dimension) / 2 - top = (height - min_dimension) / 2 - right = (width + min_dimension) / 2 - bottom = (height + min_dimension) / 2 - im = im.crop((left, top, right, bottom)) + if icon == True: + width, height = im.size + min_dimension = min(width, height) + left = (width - min_dimension) / 2 + top = (height - min_dimension) / 2 + right = (width + min_dimension) / 2 + bottom = (height + min_dimension) / 2 + im = im.crop((left, top, right, bottom)) + im.thumbnail((100, 100)) + else: + im.thumbnail((1200, 1200)) - # Resize to 100 by 100 or smaller - im.thumbnail((100, 100)) + output = io.BytesIO() + # no more webp + im.save(output, format='JPEG', quality=85) + output.seek(0) + random_name = f"{uuid.uuid4()}.jpg" + return ContentFile(output.read(), name=random_name) + except: + return image - # Compress the image - output = io.BytesIO() - im.save(output, format='WEBP', quality=85) - output.seek(0) - random_name = f"{uuid.uuid4()}.webp" - return ContentFile(output.read(), name=random_name) +class message_form(forms.ModelForm): + body = forms.CharField(max_length=2200, required=True) + file = forms.FileField(required=False) + feeling_id = forms.IntegerField(required=False) -def compress_and_resize_content(image): - im = Image.open(image) - im = im.convert('RGB') + def clean_file(self): + file = self.cleaned_data.get('file') + max_size = settings.max_file_size + if file: + if file.size > max_size: + raise ValidationError(f'File size is too large. ({max_size / 1024 /1024 }MB max)') + if file.content_type.startswith('image'): + return compress_and_resize_content(image=file) + return file + + class Meta: + model = Message + fields = ( + 'body', + 'file', + 'feeling_id', + ) - # Resize to 1,200 by 1,200 or smaller - im.thumbnail((1200, 1200)) +class comment_form(forms.ModelForm): + body = forms.CharField(max_length=2200, required=True) + file = forms.FileField(required=False) + feeling_id = forms.IntegerField(required=False) + is_spoiler = forms.BooleanField(required=False) - # Compress the image - output = io.BytesIO() - im.save(output, format='WEBP', quality=85) - output.seek(0) - random_name = f"{uuid.uuid4()}.webp" - return ContentFile(output.read(), name=random_name) + def clean_file(self): + file = self.cleaned_data.get('file') + max_size = settings.max_file_size + if file: + if file.size > max_size: + raise ValidationError(f'File size is too large. ({max_size / 1024 /1024 }MB max)') + if file.content_type.startswith('image'): + return compress_and_resize_content(image=file) + return file -# I do want to move each and every form over to here. Not only will this trivialize making new forms, this will also make it more secure or something. -class CommunitySettingForm(forms.ModelForm): + class Meta: + model = Comment + fields = ( + 'body', + 'file', + 'feeling_id', + 'is_spoiler', + ) + +class post_form(forms.ModelForm): + body = forms.CharField(max_length=2200, required=True) + url = forms.URLField(required=False) + file = forms.FileField(required=False) + feeling_id = forms.IntegerField(required=False) + is_spoiler = forms.BooleanField(required=False) + + def clean_file(self): + file = self.cleaned_data.get('file') + max_size = settings.max_file_size + if file: + if file.size > max_size: + raise ValidationError(f'File size is too large. ({max_size / 1024 /1024 }MB max)') + if file.content_type.startswith('image'): + return compress_and_resize_content(image=file) + return file + + def clean_url(self): + url = self.cleaned_data.get('url') + if url: + try: + URLValidator()(value=url) + except ValidationError: + raise ValidationError("Uh-oh, that URL wasn't valid.") + return url + + class Meta: + model = Post + fields = ( + 'body', + 'url', + 'file', + 'feeling_id', + 'is_spoiler', + ) + +class edit_community(forms.ModelForm): description = forms.CharField(max_length = 2200,required=False, widget=forms.Textarea(attrs={'class': 'textarea'})) def __init__(self, *args, **kwargs): - super(CommunitySettingForm, self).__init__(*args, **kwargs) + super(edit_community, self).__init__(*args, **kwargs) # Store the initial values of the image fields self.initial_ico = self.instance.ico self.initial_banner = self.instance.banner def clean_ico(self): ico = self.cleaned_data.get('ico') + max_size = settings.max_file_size # Check if the image has changed if ico and ico != self.initial_ico: - return compress_and_resize_icon(image=ico) + if ico.size > max_size: + raise ValidationError(f'File size is too large. ({max_size / 1024 /1024 }MB max)') + return compress_and_resize_content(image=ico, icon=True) return ico def clean_banner(self): banner = self.cleaned_data.get('banner') + max_size = settings.max_file_size # Check if the image has changed if banner and banner != self.initial_banner: + if banner.size > max_size: + raise ValidationError(f'File size is too large. ({max_size / 1024 /1024 }MB max)') return compress_and_resize_content(image=banner) return banner @@ -82,7 +161,7 @@ class CommunitySettingForm(forms.ModelForm): 'banner', ) -class Settomgs_Change_Password(forms.Form): +class set_password(forms.Form): Old_Password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'auth-input', 'placeholder': 'Old Password'})) New_Password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'auth-input', 'placeholder': 'New Password'})) Confirm_Password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'auth-input', 'placeholder': 'Confirm Password'})) @@ -103,17 +182,17 @@ class Settomgs_Change_Password(forms.Form): except forms.ValidationError as error: raise forms.ValidationError("your password fucking sucks!") return cleaned_data -class PurgeForm(forms.Form): +class purge_user(forms.Form): purge_posts = forms.BooleanField(required=False, label='Purge all posts', help_text='Purge all posts from this user.') purge_comments = forms.BooleanField(required=False, label='Purge all comments', help_text='Purge all comments from this user.') restore_all = forms.BooleanField(required=False, label='Restore purged content', help_text='Restore everything that was purged, this will not apply to posts deleted manually.') -class LoginForm(forms.Form): +class sign_in(forms.Form): username = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class': 'auth-input', 'placeholder': 'Username / Email'})) password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'auth-input', 'placeholder': 'Password'})) def clean(self): - cleaned_data = super(LoginForm, self).clean() + cleaned_data = super(sign_in, self).clean() username = cleaned_data.get('username') password = cleaned_data.get('password') @@ -131,9 +210,10 @@ class LoginForm(forms.Form): active_user_ban = Ban.objects.filter(to=user[0], active=True, expiry_date__gte=timezone.now()).first() if active_user_ban: raise forms.ValidationError("This account has been banned until {}. Reason: {}".format(active_user_ban.expiry_date, active_user_ban.reason), code='banned') + self.user = user return cleaned_data -class User_tools_Form(forms.ModelForm): +class manage_user(forms.ModelForm): username = forms.CharField(max_length = 64, required = True, help_text = 'Usernames are used when signing in, so be sure to let this user know if you change this.') has_mh = forms.BooleanField(required = False, label='Use Mii', help_text='Turn this on to use Mii Hashes instead of normal profile pictures.') avatar = forms.CharField(required = False, help_text = 'If \"Use Mii\" is turned on, The Mii Hash should reside here, otherwise any good old URL will do.') @@ -148,7 +228,7 @@ class User_tools_Form(forms.ModelForm): raise forms.ValidationError("The username contains invalid characters.") return username -class Profile_tools_Form(forms.ModelForm): +class manage_profile(forms.ModelForm): # applying classes is super weird. comment = forms.CharField(required=False, widget=forms.Textarea(attrs={'class': 'textarea'})) let_freedom = forms.BooleanField(required=False, label='Let this user upload images?', help_text='If you disable this, This user will not be able to post images, videos or make a new account.') @@ -156,22 +236,24 @@ class Profile_tools_Form(forms.ModelForm): model = Profile fields = ['comment', 'country', 'whatareyou', 'weblink', 'external', 'let_freedom', 'limit_post', 'cannot_edit'] -class Give_warning_form(forms.ModelForm): +class give_warning(forms.ModelForm): reason = forms.CharField(required=True, widget=forms.Textarea(attrs={'class': 'textarea'})) # Super simple, does not need to be it's own form but whatever. class Meta: model = Warning fields = ['reason'] -class Give_Ban_Form(forms.ModelForm): +class give_ban(forms.ModelForm): BAN_OPTIONS = [ (1, '1 Day'), (2, '2 Days'), (3, '3 Days'), (7, '1 Week'), (14, '2 Weeks'), + (21, '3 Weeks'), (30, '1 Month'), (60, '2 Months'), + (90, '3 Months'), (365, '1 Year'), (None, 'Forever'), ] @@ -189,10 +271,147 @@ class Give_Ban_Form(forms.ModelForm): model = Ban fields = ['reason', 'expiry_date'] -class Give_Ban_Form_Edit(forms.ModelForm): +class edit_ban(forms.ModelForm): reason = forms.CharField(required=True, widget=forms.Textarea(attrs={'class': 'textarea'})) expiry_date = forms.DateTimeField(widget=forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'DateTimeInput'}, format='%Y-%m-%dT%H:%M')) active = forms.BooleanField(required=False) class Meta: model = Ban - fields = ['reason', 'expiry_date', 'active'] \ No newline at end of file + fields = ['reason', 'expiry_date', 'active'] + +class profile_settings_page(forms.Form): + # This form is an absolute clusterfuck. + screen_name = forms.CharField(max_length=32, required=True) + pronouns = forms.CharField(max_length=16, required=False) + profile_comment = forms.CharField(max_length=2200, required=False) + country = forms.CharField(max_length=64, required=False) + email = forms.CharField(max_length=255, required=False) + website = forms.CharField(max_length=255, required=False) + external = forms.CharField(max_length=255, required=False) + whatareyou = forms.CharField(max_length=255, required=False) + color = forms.CharField(max_length=7, required=False) + id_visibility = forms.IntegerField(max_value=2, min_value=0) + let_friendrequest = forms.IntegerField(max_value=2, min_value=0) + yeahs_visibility = forms.IntegerField(max_value=2, min_value=0) + comments_visibility = forms.IntegerField(max_value=2, min_value=0) + theme = forms.CharField(max_length=7, required=False) + reset_theme = forms.BooleanField(required=False) + + '''PFP stuff''' + avatar = forms.IntegerField(max_value=3, min_value=0, required=False) + origin_id = forms.CharField(max_length=16, min_length=6, required=False) + mh = forms.CharField(max_length=16, required=False) + file= forms.ImageField(required=False) + + def clean_email(self): + email = self.cleaned_data.get('email') + validator = EmailValidator() + if email: + try: + validator(email) + except ValidationError: + raise forms.ValidationError("Invalid email.") + return email + return None + + def is_color_acceptable(self, hex_color): + hex_color = hex_color.lstrip('#') + r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + r /= 255.0 + g /= 255.0 + b /= 255.0 + luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b + + # Define thresholds + too_dark_threshold = 0.2 + too_bright_threshold = 0.8 + + if luminance < too_dark_threshold: + return False + elif luminance > too_bright_threshold: + return False + else: + return True + + def get_gravatar(self, email): + try: + page = urllib.request.urlopen('https://gravatar.com/avatar/'+ md5(email.encode('utf-8').lower()).hexdigest() +'?d=404&s=128') + except: + return False + return page.geturl() + + # what an abomination + def save(self, user, commit=True): + profile = user.profile() + + user.nickname = self.cleaned_data['screen_name'] + user.email = self.cleaned_data['email'] + + if not self.cleaned_data.get('origin_id'): + profile.origin_info = None + profile.origin_id = None + + avatar_option = self.cleaned_data.get('avatar') + match avatar_option: + # will add case 3 later who gives a shit + case 2: + if self.cleaned_data.get('file'): + user.avatar_upload = compress_and_resize_content(image=self.cleaned_data.get('file'), icon=True) + user.avatar_type = 0 + case 1: + user.avatar_input = util.get_gravatar(user.email) or None + user.avatar_type = 1 + case 0: + if not self.cleaned_data.get('origin_id'): + user.avatar_type = 0 + user.avatar_input = None + else: + if self.cleaned_data.get('mh'): + user.avatar_type = 2 + user.avatar_input = self.cleaned_data.get('mh') + profile.origin_id = self.cleaned_data.get('origin_id') + profile.origin_info = json.dumps([self.cleaned_data.get('mh'), 'if you see this then something is wrong', self.cleaned_data['origin_id']]) + + # Setting the username color + color = self.cleaned_data.get('color') + if color: + try: + validate_color(color) + except ValidationError: + user.color = None + else: + if self.is_color_acceptable(color): + user.color = color + else: + user.color = None + + # Setting the theme + theme = self.cleaned_data.get('theme') + if theme: + reset_theme = self.cleaned_data.get('reset_theme') + try: + validate_color(theme) + except ValidationError: + user.theme = None + else: + if reset_theme: + user.theme = None + pass + if self.is_color_acceptable(theme): + user.theme = theme + else: + user.theme = None + + profile.comment = self.cleaned_data['profile_comment'] + profile.pronoun_is = self.cleaned_data['pronouns'] + profile.country = self.cleaned_data['country'] + profile.weblink = self.cleaned_data['website'] + profile.external = self.cleaned_data['external'] + profile.whatareyou = self.cleaned_data['whatareyou'] + profile.id_visibility = self.cleaned_data['id_visibility'] + profile.let_friendrequest = self.cleaned_data['let_friendrequest'] + profile.yeahs_visibility = self.cleaned_data['yeahs_visibility'] + profile.comments_visibility = self.cleaned_data['comments_visibility'] + if commit: + user.save() + profile.save() diff --git a/closedverse_main/migrations/0002_auto_20231016_1355.py b/closedverse_main/migrations/0002_auto_20231016_1355.py new file mode 100644 index 0000000..fe5b1c2 --- /dev/null +++ b/closedverse_main/migrations/0002_auto_20231016_1355.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.2 on 2023-10-16 20:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0003_logentry_add_action_flag_choices'), + ('closedverse_main', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='userrequest', + name='user_ptr', + ), + migrations.AlterModelOptions( + name='user', + options={}, + ), + migrations.AlterField( + model_name='user', + name='is_staff', + field=models.BooleanField(default=False, help_text="Allow this user to access the admin panel you're using right now? Don't forget to specify the permissions."), + ), + migrations.DeleteModel( + name='ConversationInvite', + ), + migrations.DeleteModel( + name='UserRequest', + ), + ] diff --git a/closedverse_main/migrations/0003_auto_20231016_1410.py b/closedverse_main/migrations/0003_auto_20231016_1410.py new file mode 100644 index 0000000..3cf5802 --- /dev/null +++ b/closedverse_main/migrations/0003_auto_20231016_1410.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.2 on 2023-10-16 21:10 + +import closedverse_main.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('closedverse_main', '0002_auto_20231016_1355'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='file', + field=models.FileField(blank=True, max_length=1024, null=True, upload_to='comment_file/%Y/%m/%d/'), + ), + migrations.AddField( + model_name='message', + name='file', + field=models.FileField(blank=True, max_length=1024, null=True, upload_to='message_file/%Y/%m/%d/'), + ), + migrations.AddField( + model_name='post', + name='file', + field=models.FileField(blank=True, max_length=1024, null=True, upload_to='post_file/%Y/%m/%d/'), + ), + migrations.AddField( + model_name='user', + name='avatar_input', + field=models.CharField(blank=True, help_text='Input a Mii Hash, or URL depending on what "Avatar type" is set to..', max_length=1200, null=True), + ), + migrations.AddField( + model_name='user', + name='avatar_type', + field=models.SmallIntegerField(choices=[(0, 'ImageField'), (1, 'URL / Gravatar'), (2, 'Mii Hash'), (3, 'Mii Studio')], default=False, help_text='Determines the type of avatar this user has.'), + ), + migrations.AddField( + model_name='user', + name='avatar_upload', + field=models.ImageField(blank=True, help_text='Note this only works if you have "Avatar type" set to "ImageField"', null=True, upload_to='avatars/'), + ), + migrations.AddField( + model_name='user', + name='show_announcements', + field=models.BooleanField(default=True), + ), + ] diff --git a/closedverse_main/migrations/0004_auto_20231016_1419.py b/closedverse_main/migrations/0004_auto_20231016_1419.py new file mode 100644 index 0000000..901eec5 --- /dev/null +++ b/closedverse_main/migrations/0004_auto_20231016_1419.py @@ -0,0 +1,60 @@ +# Generated by Django 3.2.2 on 2023-10-16 21:19 + +from django.db import migrations + +def migrate_media_to_file(apps, schema_editor): + # Migrate Comment model + Comment = apps.get_model('closedverse_main', 'Comment') + for comment in Comment.objects.all(): + if comment.drawing and comment.drawing.startswith('/i/'): + comment.file = comment.drawing[3:] + comment.save() + + if comment.screenshot and comment.screenshot.startswith('/i/'): + comment.file = comment.screenshot[3:] + comment.save() + + # Migrate Message model + Message = apps.get_model('closedverse_main', 'Message') + for message in Message.objects.all(): + if message.drawing and message.drawing.startswith('/i/'): + message.file = message.drawing[3:] + message.save() + + if message.screenshot and message.screenshot.startswith('/i/'): + message.file = message.screenshot[3:] + message.save() + + # Migrate Post model + Post = apps.get_model('closedverse_main', 'Post') + for post in Post.objects.all(): + if post.drawing and post.drawing.startswith('/i/'): + post.file = post.drawing[3:] + post.save() + + if post.screenshot and post.screenshot.startswith('/i/'): + post.file = post.screenshot[3:] + post.save() + + if post.video and post.video.startswith('/i/'): + post.file = post.video[3:] + post.save() + + User = apps.get_model('closedverse_main', 'User') + for user in User.objects.all(): + user.avatar_input = user.avatar + if user.has_mh: + user.avatar_type = 2 + else: + user.avatar_type = 1 + user.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('closedverse_main', '0003_auto_20231016_1410'), + ] + + operations = [ + migrations.RunPython(migrate_media_to_file), + ] diff --git a/closedverse_main/migrations/0005_auto_20231016_1502.py b/closedverse_main/migrations/0005_auto_20231016_1502.py new file mode 100644 index 0000000..59ad4e6 --- /dev/null +++ b/closedverse_main/migrations/0005_auto_20231016_1502.py @@ -0,0 +1,113 @@ +# Generated by Django 3.2.2 on 2023-10-16 22:02 + +import closedverse_main.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('closedverse_main', '0004_auto_20231016_1419'), + ] + + operations = [ + migrations.RemoveField( + model_name='comment', + name='drawing', + ), + migrations.RemoveField( + model_name='comment', + name='screenshot', + ), + migrations.RemoveField( + model_name='message', + name='drawing', + ), + migrations.RemoveField( + model_name='message', + name='screenshot', + ), + migrations.RemoveField( + model_name='post', + name='drawing', + ), + migrations.RemoveField( + model_name='post', + name='screenshot', + ), + migrations.RemoveField( + model_name='post', + name='video', + ), + migrations.RemoveField( + model_name='profile', + name='email_login', + ), + migrations.RemoveField( + model_name='user', + name='avatar', + ), + migrations.RemoveField( + model_name='user', + name='bg_url', + ), + migrations.RemoveField( + model_name='user', + name='has_mh', + ), + migrations.AlterField( + model_name='community', + name='banner', + field=models.ImageField(blank=True, null=True, upload_to='community_banners/'), + ), + migrations.AlterField( + model_name='community', + name='ico', + field=models.ImageField(blank=True, null=True, upload_to='community_icons/'), + ), + migrations.AlterField( + model_name='message', + name='url', + field=models.URLField(blank=True, max_length=1200, null=True), + ), + migrations.AlterField( + model_name='profile', + name='comment', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='profile', + name='country', + field=models.CharField(blank=True, max_length=120), + ), + migrations.AlterField( + model_name='profile', + name='external', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AlterField( + model_name='profile', + name='pronoun_is', + field=models.CharField(blank=True, max_length=16), + ), + migrations.AlterField( + model_name='profile', + name='weblink', + field=models.CharField(blank=True, max_length=1200), + ), + migrations.AlterField( + model_name='profile', + name='whatareyou', + field=models.CharField(blank=True, max_length=120), + ), + migrations.AlterField( + model_name='user', + name='color', + field=closedverse_main.models.ColorField(blank=True, max_length=18, null=True), + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + ] diff --git a/closedverse_main/models.py b/closedverse_main/models.py index e30d048..9d113c6 100644 --- a/closedverse_main/models.py +++ b/closedverse_main/models.py @@ -17,7 +17,7 @@ import uuid, json, base64 from django.core.mail import send_mail from django.template.loader import render_to_string from django.urls import reverse -from django.db.models.signals import pre_delete +import mimetypes import re import unicodedata import random @@ -52,14 +52,14 @@ class UserManager(BaseUserManager): ) profile = Profile.objects.model() if nn: - user.avatar = nn[0] + user.avatar_input = nn[0] profile.origin_id = nn[2] profile.origin_info = json.dumps(nn) - user.has_mh = True + user.avatar_type = 2 else: - user.avatar = util.get_gravatar(email) or ('s' if getrandbits(1) else '') + user.avatar_input = util.get_gravatar(email) or ('s' if getrandbits(1) else '') - user.has_mh = False + user.avatar_type = 1 user.set_password(password) user.save(using=self._db) profile.user = user @@ -88,10 +88,6 @@ class UserManager(BaseUserManager): user = user.first() # If the user is an admin, say that they don't exist, actually no... # Or, if the user doesn't want username login, don't let them if they didn't enter their email - if user.profile('email_login') == 2 and not user.email == username: - return None - elif user.profile('email_login') == 0 and user.email == username: - return None try: passwd = user.check_password(password) # Check if the password is a valid bcrypt @@ -136,15 +132,17 @@ class Role(models.Model): # as of writing, mii-secure is unstable, nintendo please do not f*ck me for this mii_domain = 'https://s3.us-east-1.amazonaws.com/mii-images.account.nintendo.net/' +studiomii_domain = 'https://studio.mii.nintendo.com/' + class User(AbstractBaseUser, PermissionsMixin): id = models.AutoField(primary_key=True) username = models.CharField(max_length=32, unique=True) nickname = models.CharField(max_length=64, null=True) password = models.CharField(max_length=128) - email = models.EmailField(null=True, blank=True, default='') - has_mh = models.BooleanField(default=False, help_text='Don\'t touch this. This is if the user\'s avatar is set to a Mii.') - avatar = models.CharField(max_length=1200, blank=True, default='') - theme = ColorField(blank=True, null=True) + email = models.EmailField(null=True, blank=True) + avatar_type = models.SmallIntegerField(default=False, help_text='Determines the type of avatar this user has.', choices=((0, 'ImageField'), (1, 'URL / Gravatar'), (2, 'Mii Hash'), (3, 'Mii Studio'))) + avatar_input = models.CharField(max_length=1200, null=True, blank=True, help_text='Input a Mii Hash, or URL depending on what "Avatar type" is set to..') + avatar_upload = models.ImageField(blank=True, null=True, upload_to='avatars/', help_text='Note this only works if you have "Avatar type" set to "ImageField"') # LEVEL: 0-1 is default, everything else is just levels level = models.SmallIntegerField(default=0, help_text='This is the level of authority. People with a lower level cannot edit those with a higher level. This also grants additional permissions outside of the Django Admin Panel.') role = models.ForeignKey(Role, blank=True, null=True, on_delete=models.SET_NULL, help_text='This will show a funny badge and text on this user\'s profile. This does not grant the user any additional power and is only visual.') @@ -153,15 +151,16 @@ class User(AbstractBaseUser, PermissionsMixin): # C Tokens are things that let you make communities and shit. c_tokens = models.IntegerField(default=1, help_text='How many communities should this user be allowed to make?') - # Things that don't have to do with auth lol + # Personalization stuff hide_online = models.BooleanField(default=False, help_text='If this is ticked, the user has opted to hide their online status.') - color = ColorField(default='', null=True, blank=True) + color = ColorField(null=True, blank=True) + theme = ColorField(blank=True, null=True) + show_announcements = models.BooleanField(default=True) is_staff = models.BooleanField(default=False, help_text='Allow this user to access the admin panel you\'re using right now? Don\'t forget to specify the permissions.') is_active = models.BooleanField(default=True, help_text='If this is off, the user is basically banned and can\'t do shit here') is_superuser = models.BooleanField(default=False, help_text='Overrides django groups. This also allows you to change people\'s perms.') can_invite = models.BooleanField(default=True, help_text='Can this user invite new users? This does not matter unless the invite system is turned on.') - bg_url = models.CharField(max_length=300, null=True, blank=True) is_anonymous = False is_authenticated = True @@ -217,11 +216,10 @@ class User(AbstractBaseUser, PermissionsMixin): return infodecode[0] def unread_warning(self): return Notification.objects.filter(type=5, to=self, read=False).exists() - def has_plain_avatar(self): - if not self.has_mh and '/' in self.avatar and not 'gravatar.com' in self.avatar: - return True def has_avatar(self): - if not self.avatar or len(self.avatar) == 1: + if self.avatar_type == 0 and not self.avatar_upload: + return False + if not self.avatar_type == 0 and not self.avatar_input: return False return True def let_yeahnotifs(self): @@ -233,10 +231,12 @@ class User(AbstractBaseUser, PermissionsMixin): return False today_min = timezone.datetime.combine(timezone.datetime.today(), time.min) today_max = timezone.datetime.combine(timezone.datetime.today(), time.max) - # recent_posts = - # Posts made by the user today + posts made by the IP today + - # same thing except with comments - recent_posts = Post.real.filter(Q(creator=self.id, created__range=(today_min, today_max)) | Q(creator__addr=self.addr, created__range=(today_min, today_max))).count() + Comment.real.filter(Q(creator=self.id, created__range=(today_min, today_max)) | Q(creator__addr=self.addr, created__range=(today_min, today_max))).count() + # recent_posts + recent_posts = Post.real.filter( + Q(creator=self) & Q(created__range=(today_min, today_max)) + ).count() + Comment.real.filter( + Q(creator=self) & Q(created__range=(today_min, today_max)) + ).count() # Posts remaining return int(limit) - recent_posts @@ -268,24 +268,46 @@ class User(AbstractBaseUser, PermissionsMixin): else: return True def do_avatar(self, feeling=0): - if self.has_mh and self.avatar: - feeling = { - 0: 'normal', - 1: 'happy', - 2: 'like', - 3: 'surprised', - 4: 'frustrated', - 5: 'puzzled', - }.get(feeling, "normal") - url = '{2}{0}_{1}_face.png'.format(self.avatar, feeling, mii_domain) - return url - elif not self.avatar: - return settings.STATIC_URL + 'img/anonymous-mii.png' - elif self.avatar == 's': - return settings.STATIC_URL + 'img/anonymous-mii-sad.png' - else: - return self.avatar - + anon = settings.STATIC_URL + 'img/anonymous-mii.png' + match self.avatar_type: + case 3: + if self.avatar_input: + feeling = { + 0: 'normal', + 1: 'smile_open_mouth', + 2: 'like_wink_left', + 3: 'surprise_open_mouth', + 4: 'frustrated', + 5: 'sorrow', + }.get(feeling, "normal") + url = '{2}miis/image.png?data={0}&type=face&expression={1}&width=128'.format(self.avatar_input, feeling, studiomii_domain) + return url + else: + return anon + case 2: + if self.avatar_input: + feeling = { + 0: 'normal', + 1: 'happy', + 2: 'like', + 3: 'surprised', + 4: 'frustrated', + 5: 'puzzled', + }.get(feeling, "normal") + url = '{2}{0}_{1}_face.png'.format(self.avatar_input, feeling, mii_domain) + return url + else: + return anon + case 1: + if self.avatar_input: + return self.avatar_input + else: + return anon + case 0: + if self.avatar_upload: + return self.avatar_upload.url + else: + return anon def num_yeahs(self): return self.yeah_set.count() def num_posts(self): @@ -519,18 +541,12 @@ class User(AbstractBaseUser, PermissionsMixin): return self.save(update_fields=['addr', 'last_login']) return self.save(update_fields=['last_login']) - def has_postspam(self, body, screenshot=None, drawing=None): + def has_postspam(self, body, file=None): latest_post = self.post_set.order_by('-created')[:1] if not latest_post: return False latest_post = latest_post.first() - if drawing and latest_post.drawing: - if drawing == latest_post.drawing: - return True - elif latest_post.screenshot and screenshot and not drawing: - if latest_post.screenshot == screenshot and latest_post.body == body: - return True - elif latest_post.body and body and not latest_post.screenshot and not latest_post.drawing: + if latest_post.body and body and not latest_post.file: if latest_post.body == body: return True return False @@ -654,8 +670,8 @@ class Community(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=255) description = models.TextField(blank=True) - ico = models.ImageField(null=True, blank=True) - banner = models.ImageField(null=True, blank=True) + ico = models.ImageField(null=True, blank=True, upload_to='community_icons/') + banner = models.ImageField(null=True, blank=True, upload_to='community_banners/') # Type: 0 - general, 1 - game, 2 - special type = models.SmallIntegerField(default=0, help_text='The category the community belongs in, setting this to None will remove the community.', choices=((0, 'General'), (1, 'Game'), (2, 'Special'), (3, 'User Community'), (4, 'Hide'))) platform = models.SmallIntegerField(default=0, choices=((0, 'None'), (1, '3DS'), (2, 'Wii U'), (3, 'Switch'), (4, '3DS and Wii U'), (5, 'PC'), (6, 'Xbox'), (7, 'Playstation'))) @@ -788,67 +804,11 @@ class Community(models.Model): if self.has_favorite(request): return request.user.communityfavorite_set.filter(community=self).delete() - def setup(self, request): if request.user.is_authenticated: self.post_perm = self.post_perm(request) self.has_favorite = self.has_favorite(request) - def create_post(self, request): - if not self.post_perm(request): - return 4 - limit = request.user.limit_remaining() - if not limit is False and not limit > 0: - return 8 - del(limit) - #if Post.real.filter(Q(creator=request.user) | Q(creator__addr=request.user.addr), created__gt=timezone.now() - timedelta(seconds=10)).exists(): - # return 3 - if request.POST.get('url'): - try: - URLValidator()(value=request.POST['url']) - except ValidationError: - return 5 - if not request.user.has_freedom() and (request.POST.get('url') or request.FILES.get('screen') or request.FILES.get('video')): - return 6 - if not request.user.is_active: - return 6 - if request.user.unread_warning(): - return 13 - if len(request.POST['body']) > 2200 or (len(request.POST['body']) < 1 and not request.POST.get('_post_type') == 'painting'): - return 1 - upload = None - drawing = None - video = None - body = request.POST.get('body') - for c in body: - if unicodedata.combining(c): - return 12 - if request.FILES.get('screen'): - upload = util.image_upload(request.FILES['screen'], True) - if upload == 1: - return 2 - if request.FILES.get('video'): - video = util.video_upload(request.FILES['video']) - if video == 1: - return 11 - if request.POST.get('_post_type') == 'painting': - if not request.POST.get('painting'): - return 2 - drawing = util.image_upload(request.POST['painting'], False, True) - if drawing == 1: - return 2 - # Check for spam using our OWN ALGO!!!!!!!!! - if request.user.has_postspam(body, upload, drawing): - return 7 - for keyword in ['faggot', 'fag', 'nigger', 'nigga']: - if keyword in body.lower(): - return 9 - if body.isspace() and not drawing: - return 10 - new_post = self.post_set.create(body=body, creator=request.user, community=self, feeling=int(request.POST.get('feeling_id', 0)), spoils=bool(request.POST.get('is_spoiler')), screenshot=upload, drawing=drawing, url=request.POST.get('url'), video=video) - new_post.is_mine = True - return new_post - def search(query='', limit=50, offset=0, request=None): return Community.objects.filter(Q(name__icontains=query) | Q(description__contains=query)).exclude(type=4).order_by('-created')[offset:offset + limit] @@ -872,9 +832,7 @@ class Post(models.Model): community = models.ForeignKey(Community, null=True, on_delete=models.CASCADE) feeling = models.SmallIntegerField(default=0, choices=feelings) body = models.TextField(null=True) - drawing = models.CharField(max_length=200, null=True, blank=True) - screenshot = models.CharField(max_length=1200, null=True, blank=True, default='') - video = models.CharField(max_length=256, null=True, blank=True, default='') + file = models.FileField(max_length=1024, null=True, blank=True, upload_to='post_file/%Y/%m/%d/') url = models.URLField(max_length=1200, null=True, blank=True, default='') spoils = models.BooleanField(default=False) disable_yeah = models.BooleanField(default=False) @@ -898,8 +856,6 @@ class Post(models.Model): def trun(self): if self.is_rm: return 'deleted' - if self.drawing: - return 'drawing' else: return self.body def yt_vid(self): @@ -917,6 +873,26 @@ class Post(models.Model): return (self.creator == user) else: return False + def file_type(self): + mimetypes.add_type("image/webp", ".webp") + # Get the file MIME type + if not self.file: + return None + mime_type, encoding = mimetypes.guess_type(self.file.name) + if mime_type: + # Determine the type of file + type_main, type_sub = mime_type.split('/') + if type_main == 'image': + return 1 + elif type_main == 'video': + return 2 + elif type_main == 'audio': + return 3 + else: + return 0 + else: + return 0 + #def yeah_notification(self, request): # ???? What is this #Notification.give_notification @@ -1027,43 +1003,6 @@ class Post(models.Model): for post in comments: post.setup(request) return comments - def create_comment(self, request): - if not self.can_comment(request): - return False - limit = request.user.limit_remaining() - if not limit is False and not limit > 0: - return 8 - del(limit) - if self.is_mine(request.user) and Comment.real.filter(creator=request.user, created__gt=timezone.now() - timedelta(seconds=2)).exists(): - return 3 - elif not self.is_mine(request.user) and Comment.real.filter(creator=request.user, created__gt=timezone.now() - timedelta(seconds=10)).exists(): - return 3 - if not request.user.has_freedom() and (request.POST.get('url') or request.FILES.get('screen')): - return 6 - for c in request.POST['body']: - if unicodedata.combining(c): - return 12 - if not request.user.is_active: - return 6 - if len(request.POST['body']) > 2200 or (len(request.POST['body']) < 1 and not request.POST.get('_post_type') == 'painting'): - return 1 - if request.user.unread_warning(): - return 13 - upload = None - drawing = None - if request.FILES.get('screen'): - upload = util.image_upload(request.FILES['screen'], True) - if upload == 1: - return 2 - if request.POST.get('_post_type') == 'painting': - if not request.POST.get('painting'): - return 2 - drawing = util.image_upload(request.POST['painting'], False, True) - if drawing == 1: - return 2 - new_post = self.comment_set.create(body=request.POST.get('body'), creator=request.user, community=self.community, original_post=self, feeling=int(request.POST.get('feeling_id', 0)), spoils=bool(request.POST.get('is_spoiler')), drawing=drawing, screenshot=upload) - new_post.is_mine = True - return new_post def recent_comment(self): if self.number_comments() < 1: return False @@ -1118,12 +1057,6 @@ class Post(models.Model): else: self.status = 2 AuditLog.objects.create(type=0, post=self, user=self.creator, by=request.user) - if self.screenshot: - util.image_rm(self.screenshot) - self.screenshot = None - if self.drawing: - util.image_rm(self.drawing) - self.drawing = None self.save() def setup(self, request): self.has_yeah = self.has_yeah(request) @@ -1143,8 +1076,7 @@ class Comment(models.Model): community = models.ForeignKey(Community, on_delete=models.CASCADE) feeling = models.SmallIntegerField(default=0, choices=feelings) body = models.TextField(null=True) - screenshot = models.CharField(max_length=1200, null=True, blank=True, default='') - drawing = models.CharField(max_length=200, null=True, blank=True) + file = models.FileField(max_length=1024, null=True, blank=True, upload_to='comment_file/%Y/%m/%d/') spoils = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) edited = models.DateTimeField(auto_now=True) @@ -1164,10 +1096,27 @@ class Comment(models.Model): def trun(self): if self.is_rm: return 'deleted' - if self.drawing: - return '(drawing)' else: return self.body + def file_type(self): + mimetypes.add_type("image/webp", ".webp") + # Get the file MIME type + if not self.file: + return None + mime_type, encoding = mimetypes.guess_type(self.file.name) + if mime_type: + # Determine the type of file + type_main, type_sub = mime_type.split('/') + if type_main == 'image': + return 1 + elif type_main == 'video': + return 2 + elif type_main == 'audio': + return 3 + else: + return 0 + else: + return 0 def is_mine(self, user): if user.is_authenticated: return (self.creator == user) @@ -1238,12 +1187,6 @@ class Comment(models.Model): else: self.status = 2 AuditLog.objects.create(type=1, comment=self, user=self.creator, by=request.user) - if self.screenshot: - util.image_rm(self.screenshot) - self.screenshot = None - if self.drawing: - util.image_rm(self.drawing) - self.drawing = None self.save() def setup(self, request): self.has_yeah = self.has_yeah(request) @@ -1277,23 +1220,19 @@ class Profile(models.Model): origin_id = models.CharField(max_length=16, null=True, blank=True) origin_info = models.CharField(max_length=255, null=True, blank=True) - comment = models.TextField(blank=True, default='') - country = models.CharField(max_length=120, blank=True, default='') - whatareyou = models.CharField(max_length=120, blank=True, default='') + comment = models.TextField(blank=True, null=True) + country = models.CharField(max_length=120, blank=True) + whatareyou = models.CharField(max_length=120, blank=True) #birthday = models.DateField(null=True, blank=True) id_visibility = models.SmallIntegerField(default=0, choices=visibility) - pronoun_is = models.IntegerField(default=0, help_text='haha attack helicopter.', choices=( - (0, "Not specified"), (1, "He/him"), (2, "She/her"), (3, "He/she"), (4, "It"), (5, "They/Them") - )) + pronoun_is = models.CharField(max_length=16, blank=True) let_friendrequest = models.SmallIntegerField(default=0, choices=visibility) yeahs_visibility = models.SmallIntegerField(default=0, choices=visibility) comments_visibility = models.SmallIntegerField(default=2, choices=visibility) - #relationship_visibility = models.SmallIntegerField(default=0, choices=visibility) - weblink = models.CharField(max_length=1200, blank=True, default='') - #gameskill = models.SmallIntegerField(default=0) - external = models.CharField(max_length=255, blank=True, default='') + weblink = models.CharField(max_length=1200, blank=True) + external = models.CharField(max_length=255, blank=True) favorite = models.ForeignKey(Post, blank=True, null=True, on_delete=models.SET_NULL) let_yeahnotifs = models.BooleanField(default=True) @@ -1302,9 +1241,7 @@ class Profile(models.Model): #let_avatar = models.BooleanField(default=False) # Post limit, 0 for none limit_post = models.SmallIntegerField(default=0, help_text='Great for spammers, set to \"0\" to remove the restriction.') - # If this is true, the user can't change their avatar or nickname cannot_edit = models.BooleanField(default=False, help_text='Make it so this user cannot change settings.') - email_login = models.SmallIntegerField(default=1, choices=((0, 'Do not allow'), (1, 'Okay'), (2, 'Only allow'))) def __str__(self): return "profile for " + self.user.username @@ -1609,38 +1546,13 @@ class Conversation(models.Model): for msg in msgs: msg.mine = msg.mine(request.user) return msgs - def make_message(self, request): - if not request.user.has_freedom() and (request.POST.get('url') or request.FILES.get('screen')): - return 6 - if Message.real.filter(creator=request.user, created__gt=timezone.now() - timedelta(seconds=2)).exists(): - return 3 - if len(request.POST['body']) > 50000 or (len(request.POST['body']) < 1 and not request.POST.get('_post_type') == 'painting'): - return 1 - if request.user.unread_warning(): - return 4 - upload = None - drawing = None - if request.FILES.get('screen'): - upload = util.image_upload(request.FILES['screen'], True) - if upload == 1: - return 2 - if request.POST.get('_post_type') == 'painting': - if not request.POST.get('painting'): - return 2 - drawing = util.image_upload(request.POST['painting'], False, True) - if drawing == 1: - return 2 - new_post = self.message_set.create(body=request.POST.get('body'), creator=request.user, feeling=int(request.POST.get('feeling_id', 0)), drawing=drawing, screenshot=upload) - new_post.mine = True - return new_post class Message(models.Model): id = models.AutoField(primary_key=True) conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE) feeling = models.SmallIntegerField(default=0, choices=feelings) body = models.TextField(null=True) - drawing = models.CharField(max_length=200, null=True, blank=True) - screenshot = models.CharField(max_length=1200, null=True, blank=True, default='') - url = models.URLField(max_length=1200, null=True, blank=True, default='') + file = models.FileField(max_length=1024, null=True, blank=True, upload_to='message_file/%Y/%m/%d/') + url = models.URLField(max_length=1200, null=True, blank=True) created = models.DateTimeField(auto_now_add=True) read = models.BooleanField(default=False) is_rm = models.BooleanField(default=False) @@ -1654,10 +1566,27 @@ class Message(models.Model): def trun(self): if self.is_rm: return 'deleted' - if self.drawing: - return '(drawing)' else: return self.body + def file_type(self): + mimetypes.add_type("image/webp", ".webp") + # Get the file MIME type + if not self.file: + return None + mime_type, encoding = mimetypes.guess_type(self.file.name) + if mime_type: + # Determine the type of file + type_main, type_sub = mime_type.split('/') + if type_main == 'image': + return 1 + elif type_main == 'video': + return 2 + elif type_main == 'audio': + return 3 + else: + return 0 + else: + return 0 def mine(self, user): if self.creator == user: return True @@ -1665,14 +1594,7 @@ class Message(models.Model): def rm(self, request): if self.conversation.source == request.user or self.conversation.target == request.user: self.is_rm = True - if self.screenshot: - util.image_rm(self.screenshot) - self.screenshot = None - if self.drawing: - util.image_rm(self.drawing) - self.drawing = None self.save() - def makeopt(ls): if len(ls) < 1: raise ValueError @@ -1843,12 +1765,10 @@ class ProfileHistory(models.Model): # blah blah blah # this method will be executed when... -def rm_post_image(sender, instance, **kwargs): - if instance.screenshot: - util.image_rm(instance.screenshot) - if instance.drawing: - util.image_rm(instance.drawing) +'''def rm_post_image(sender, instance, **kwargs): + if instance.file: + util.image_rm(instance.file) # when pre_delete happens on these pre_delete.connect(rm_post_image, sender=Post) pre_delete.connect(rm_post_image, sender=Comment) -pre_delete.connect(rm_post_image, sender=Message) +pre_delete.connect(rm_post_image, sender=Message)''' diff --git a/closedverse_main/templates/closedverse_main/comment-view.html b/closedverse_main/templates/closedverse_main/comment-view.html index c86cbf3..01b32c1 100644 --- a/closedverse_main/templates/closedverse_main/comment-view.html +++ b/closedverse_main/templates/closedverse_main/comment-view.html @@ -60,13 +60,13 @@ {% endif %}
This comment contains spoilers.
diff --git a/closedverse_main/templates/closedverse_main/elements/post-form.html b/closedverse_main/templates/closedverse_main/elements/post-form.html index 9d5dc03..e4c298a 100644 --- a/closedverse_main/templates/closedverse_main/elements/post-form.html +++ b/closedverse_main/templates/closedverse_main/elements/post-form.html @@ -10,19 +10,6 @@ {% feeling_selector %}