import os import io import re import uuid import shutil import random import asyncio from datetime import datetime import requests from PIL import Image as PILImage from sqlalchemy.exc import IntegrityError from werkzeug.exceptions import BadRequest from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.utils import secure_filename from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect from flask_wtf.file import FileAllowed from flask_wtf.csrf import validate_csrf from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp from dotenv import load_dotenv, find_dotenv import aiofiles.os from sqlalchemy import func, or_ import magic from config import Config from models import db, bcrypt, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicPage, ComicVotes, Cookies, Views, Item, UserItem, Post, Subscription, EditTagsForm, EditVideoForm, RegistrationForm, LoginForm, EmptyForm from admin import register_admin_routes from upload import upload_bp from utils import allowed_file, check_file_content, check_file_size, convert_to_webp, generate_unique_filename, get_content_query, get_client_ip, get_autocomplete_suggestions from auth import auth_bp app = Flask(__name__) csrf = CSRFProtect(app) app.config.from_object(Config) db.init_app(app) bcrypt.init_app(app) login_manager = LoginManager(app) login_manager.login_view = 'login' @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) register_admin_routes(app) app.register_blueprint(upload_bp) app.register_blueprint(auth_bp) @app.route('/') def index(): page = request.args.get('page', 1, type=int) search_query = request.args.get('search') subscriptions = [] if current_user.is_authenticated: subscriptions = [ sub.author.username for sub in Subscription.query.filter_by(user_id=current_user.id).all() ] query = get_content_query(Image, subscriptions, search_query) pagination = query.paginate(page=page, per_page=25, error_out=False) user_cookies = 0 if current_user.is_authenticated: user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() user_cookies = user_cookies_record.cookies if user_cookies_record else 0 return render_template( 'index.html', images=pagination.items, pagination=pagination, user_cookies=user_cookies, search_query=search_query, content_type='art' ) @app.route('/vote_art/', methods=['POST']) @login_required def vote_art(image_id): image = Image.query.get_or_404(image_id) user_cookies = Cookies.query.filter_by(username=current_user.username).first() if image.username == current_user.username: return redirect(url_for('view', content_type='art', id=image_id)) if user_cookies and user_cookies.cookies > 0: existing_vote = Votes.query.filter_by(username=current_user.username, image_id=image_id).first() if not existing_vote: user_cookies.cookies -= 1 image.cookie_votes += 1 new_vote = Votes(username=current_user.username, image_id=image.id) db.session.add(new_vote) db.session.commit() return redirect(url_for('view', content_type='art', id=image_id)) @app.route('/view//', methods=['GET', 'POST']) @app.route('/view//', methods=['GET', 'POST']) def view(content_type, id): comments = [] avatars = {user.username: user.avatar_file for user in User.query.all()} cu = current_user.username if current_user.is_authenticated else None user_cookies = 0 if current_user.is_authenticated: user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() user_cookies = user_cookies_record.cookies if user_cookies_record else 0 if content_type == 'art': content = Image.query.get_or_404(id) comments = Comments.query.filter_by(image_id=id).order_by(Comments.comment_date.desc()).all() search_query = request.args.get('search') page = request.args.get('page', 1, type=int) subscriptions = [] if current_user.is_authenticated: subscriptions = [ sub.author.username for sub in Subscription.query.filter_by(user_id=current_user.id).all() ] query = get_content_query(Image, subscriptions, search_query) all_images = query.all() image_ids = [image.id for image in all_images] current_index = image_ids.index(id) prev_index = (current_index - 1) % len(image_ids) next_index = (current_index + 1) % len(image_ids) prev_content = all_images[prev_index] next_content = all_images[next_index] random_content = random.choice(all_images) elif content_type == 'video': content = Video.query.get_or_404(id) comments = Comments.query.filter_by(video_id=id).order_by(Comments.comment_date.desc()).all() all_videos = Video.query.order_by(Video.id).all() video_ids = [video.id for video in all_videos] current_index = video_ids.index(id) prev_index = (current_index - 1) % len(all_videos) next_index = (current_index + 1) % len(all_videos) prev_content = all_videos[prev_index] next_content = all_videos[next_index] random_content = random.choice(all_videos) elif content_type == 'comic': content = Comic.query.get_or_404(id) comments = Comments.query.filter_by(comic_id=id).order_by(Comments.comment_date.desc()).all() comic_pages_dir = os.path.join(app.config['UPLOAD_FOLDER']['comics'], content.comic_folder) if not os.path.exists(comic_pages_dir): return render_template('error.html', message="Comic pages not found") comic_pages = os.listdir(comic_pages_dir) comic_pages = sorted(comic_pages) all_comics = Comic.query.order_by(Comic.id).all() comic_ids = [comic.id for comic in all_comics] current_index = comic_ids.index(id) prev_index = (current_index - 1) % len(all_comics) next_index = (current_index + 1) % len(all_comics) prev_content = all_comics[prev_index] next_content = all_comics[next_index] random_content = random.choice(all_comics) else: abort(404) if request.method == 'POST' and current_user.is_authenticated: comment_text = request.form.get('comment') max_comment_length = 44 if comment_text and len(comment_text) > max_comment_length: return redirect(url_for('view', content_type=content_type, id=id)) if comment_text: new_comment = None if content_type == 'art': new_comment = Comments(username=cu, image_id=id, comment_text=comment_text) elif content_type == 'video': new_comment = Comments(username=cu, video_id=id, comment_text=comment_text) elif content_type == 'comic': new_comment = Comments(username=cu, comic_id=id, comment_text=comment_text) if new_comment: db.session.add(new_comment) db.session.commit() return redirect(url_for('view', content_type=content_type, id=id)) return render_template( 'view.html', content=content, content_type=content_type, comments=comments, avatars=avatars, prev_content=prev_content, next_content=next_content, random_content=random_content, user_cookies=user_cookies, comic_pages=comic_pages if content_type == 'comic' else None ) @app.route('/image_edit/', methods=['GET', 'POST']) @login_required def image_edit(id): image = Image.query.get_or_404(id) if image.username != current_user.username: return redirect(url_for('index')) form = EditTagsForm() if form.validate_on_submit(): image.tags = form.tags.data db.session.commit() return redirect(url_for('view', content_type='art', id=id)) if request.method == 'GET': form.tags.data = image.tags image_preview_url = url_for('static', filename=f'arts/{image.image_file}') return render_template( 'image_edit.html', form=form, image=image, image_preview_url=image_preview_url ) @app.route('/video_edit/', methods=['GET', 'POST']) @login_required def video_edit(id): video = Video.query.get_or_404(id) if video.username != current_user.username: return redirect(url_for('index')) form = EditVideoForm() if request.method == 'GET': form.video_name.data = video.video_name form.description.data = video.description form.tags.data = video.tags if form.validate_on_submit(): video.video_name = form.video_name.data video.description = form.description.data video.tags = form.tags.data if form.video_thumbnail.data: thumbnail_file = form.video_thumbnail.data if allowed_file(thumbnail_file.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): thumbnail_filename = generate_unique_filename(thumbnail_file.filename, app.config['UPLOAD_FOLDER']['thumbnails']) thumbnail_path = os.path.join(app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename) thumbnail_file.save(thumbnail_path) video.video_thumbnail_file = thumbnail_filename db.session.commit() return redirect(url_for('view', content_type='video', id=id)) video_preview_url = url_for('static', filename=f'videos/{video.video_thumbnail_file}') return render_template( 'video_edit.html', form=form, video=video, video_preview_url=video_preview_url ) @app.route('/navbar') def navbar(): return render_template( 'navbar.html' ) @app.route('/card') def card(): return render_template( 'card.html' ) @app.route('/autocomplete') def autocomplete(): search_query = request.args.get('search', '', type=str) if search_query: suggestions = get_autocomplete_suggestions(search_query) else: suggestions = [] return jsonify(suggestions) @app.route('/user_pubs//') def user_pubs(pub_type, username): p = request.args.get('page', 1, type=int) search_query = request.args.get('search', '') if pub_type == 'arts': query = Image.query.filter_by(username=username) if search_query: query = query.filter(or_( Image.image_file.ilike(f'%{search_query}%'), Image.tags.ilike(f'%{search_query}%') )) items = query.order_by(Image.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) elif pub_type == 'videos': query = Video.query.filter_by(username=username) if search_query: query = query.filter(Video.video_name.ilike(f'%{search_query}%')) items = query.order_by(Video.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) elif pub_type == 'comics': query = Comic.query.filter_by(username=username) if search_query: query = query.filter(Comic.name.ilike(f'%{search_query}%')) items = query.order_by(Comic.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) else: abort(404) user_cookies = Cookies.query.filter_by(username=username).first() cookies_count = user_cookies.cookies if user_cookies else 0 return render_template( 'user_pubs.html', items=items.items, pagination=items, pub_type=pub_type, username=username, cookies_count=cookies_count, search_query=search_query ) @app.route('/delete//', methods=['POST']) @login_required def delete(content_type, content_id): if content_type == 'art': content = Image.query.get_or_404(content_id) file_path = os.path.join(app.config['UPLOAD_FOLDER']['arts'], content.image_file) elif content_type == 'video': content = Video.query.get_or_404(content_id) file_path = os.path.join(app.config['UPLOAD_FOLDER']['videos'], content.video_file) elif content_type == 'comic': content = Comic.query.get_or_404(content_id) file_path = os.path.join(app.config['UPLOAD_FOLDER']['comics'], content.comic_folder) else: abort(404, "Invalid content type") if content.username != current_user.username: abort(403) if os.path.exists(file_path): if os.path.isdir(file_path): shutil.rmtree(file_path) else: os.remove(file_path) db.session.delete(content) db.session.commit() return redirect(url_for('profile', username=current_user.username)) @app.route('/delete_comment/', methods=['POST']) @login_required def delete_comment(comment_id): comment = db.session.get(Comments, comment_id) or abort(404) if comment.image_id: content_type = 'art' content_id = comment.image_id elif comment.video_id: content_type = 'video' content_id = comment.video_id else: content_type = 'comic' content_id = comment.comic_id if comment.username == current_user.username: try: if hasattr(comment, 'post_id') and comment.post_id: post_id = comment.post_id db.session.delete(comment) db.session.commit() return redirect(url_for('user_posts', username=current_user.username)) db.session.delete(comment) db.session.commit() return redirect(url_for('view', content_type=content_type, id=content_id)) except IntegrityError: db.session.rollback() return redirect(url_for('view', content_type=content_type, id=content_id)) return redirect(request.referrer or url_for('index')) @app.route('/videos') def videos(): page = request.args.get('page', 1, type=int) search_query = request.args.get('search') subscriptions = [] if current_user.is_authenticated: subscriptions = [sub.author_id for sub in Subscription.query.filter_by(user_id=current_user.id).all()] query = get_content_query(Video, subscriptions, search_query) pagination = query.paginate(page=page, per_page=10, error_out=False) user_cookies = 0 if current_user.is_authenticated: user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() user_cookies = user_cookies_record.cookies if user_cookies_record else 0 return render_template( 'videos.html', videos=pagination.items, pagination=pagination, user_cookies=user_cookies, search_query=search_query, content_type='video' ) @app.route('/vote_video/', methods=['POST']) @login_required def vote_video(video_id): video = Video.query.get_or_404(video_id) user_cookies = Cookies.query.filter_by(username=current_user.username).first() if video.username == current_user.username: return redirect(url_for('view', content_type='video', id=video_id)) if user_cookies and user_cookies.cookies > 0: existing_vote = VideoVotes.query.filter_by(username=current_user.username, video_id=video_id).first() if not existing_vote: user_cookies.cookies -= 1 video.cookie_votes += 1 new_vote = VideoVotes(username=current_user.username, video_id=video.id) db.session.add(new_vote) db.session.commit() return redirect(url_for('view', content_type='video', id=video_id)) @app.route('/comics') def comics(): page = request.args.get('page', 1, type=int) search_query = request.args.get('search') subscriptions = [] if current_user.is_authenticated: subscriptions = [sub.author_id for sub in Subscription.query.filter_by(user_id=current_user.id).all()] query = get_content_query(Comic, subscriptions, search_query) pagination = query.paginate(page=page, per_page=10, error_out=False) user_cookies = 0 if current_user.is_authenticated: user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() user_cookies = user_cookies_record.cookies if user_cookies_record else 0 return render_template( 'comics.html', comics=pagination.items, pagination=pagination, user_cookies=user_cookies, search_query=search_query, content_type='comic' ) @app.route('/vote_comic/', methods=['POST']) @login_required def vote_comic(comic_id): comic = Comic.query.get_or_404(comic_id) user_cookies = Cookies.query.filter_by(username=current_user.username).first() if comic.username == current_user.username: return redirect(url_for('view', content_type='comic', id=comic_id)) if user_cookies and user_cookies.cookies > 0: existing_vote = ComicVotes.query.filter_by(username=current_user.username, comic_id=comic_id).first() if not existing_vote: user_cookies.cookies -= 1 comic.cookie_votes += 1 new_vote = ComicVotes(username=current_user.username, comic_id=comic.id) db.session.add(new_vote) db.session.commit() return redirect(url_for('view', content_type='comic', id=comic_id)) @app.route('/comic_edit/', methods=['GET', 'POST']) @login_required async def comic_edit(comic_id): comic = Comic.query.get_or_404(comic_id) if comic.username != current_user.username: return redirect(url_for('index')) cfp = os.path.join(app.config['UPLOAD_FOLDER']['comics'], comic.name) if not os.path.exists(cfp): os.makedirs(cfp) form = EmptyForm() if request.method == 'POST' and form.validate_on_submit(): action = request.form.get('action') if action == 'delete' and (page_id := request.form.get('page')): page = ComicPage.query.get(page_id) if page: os.remove(page.file_path) db.session.delete(page) db.session.commit() elif action == 'update' and (page_id := request.form.get('page')) and 'new_page' in request.files: new_page = request.files['new_page'] filename = f"{uuid.uuid4().hex}.webp" file_path = os.path.join(cfp, filename) webp_image = await convert_to_webp(new_page) async with aiofiles.open(file_path, 'wb') as f: await f.write(webp_image.read()) page = ComicPage.query.get(page_id) if page: os.remove(page.file_path) page.file_path = file_path db.session.commit() elif action == 'add' and 'new_page' in request.files: new_page = request.files['new_page'] filename = f"{uuid.uuid4().hex}.webp" file_path = os.path.join(cfp, filename) webp_image = await convert_to_webp(new_page) async with aiofiles.open(file_path, 'wb') as f: await f.write(webp_image.read()) page_number = (db.session.query(db.func.max(ComicPage.page_number)).filter_by(comic_id=comic.id).scalar() or 0) + 1 db.session.add(ComicPage(comic_id=comic.id, page_number=page_number, file_path=file_path)) db.session.commit() return redirect(url_for('comic_edit', comic_id=comic.id)) return render_template('comic_edit.html', comic=comic, comic_pages=comic.pages, form=form) def update_related_tables(old_username, new_username): Comments.query.filter_by(username=old_username).update({"username": new_username}) Image.query.filter_by(username=old_username).update({"username": new_username}) Video.query.filter_by(username=old_username).update({"username": new_username}) Comic.query.filter_by(username=old_username).update({"username": new_username}) Cookies.query.filter_by(username=old_username).update({"username": new_username}) Post.query.filter_by(username=old_username).update({"username": new_username}) Subscription.query.filter_by(user_id=old_username).update({"user_id": new_username}) Subscription.query.filter_by(author_id=old_username).update({"author_id": new_username}) UserItem.query.filter_by(username=old_username).update({"username": new_username}) db.session.commit() @app.route('/profile_edit', methods=['GET', 'POST']) @login_required def profile_edit(): if request.method == 'POST': section = request.form.get('section') if section == 'avatar_banner': avatar = request.files.get('avatar') banner = request.files.get('banner') if avatar and allowed_file(avatar.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): if check_file_size(avatar, app.config['MAX_IMAGE_SIZE']): avatar_filename = secure_filename(avatar.filename) avatar_path = os.path.join(app.config['UPLOAD_FOLDER']['avatars'], avatar_filename) avatar.save(avatar_path) current_user.avatar_file = avatar_filename else: return redirect(url_for('profile_edit')) if banner and allowed_file(banner.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): if check_file_size(banner, app.config['MAX_IMAGE_SIZE']): banner_filename = secure_filename(banner.filename) banner_path = os.path.join(app.config['UPLOAD_FOLDER']['banners'], banner_filename) banner.save(banner_path) current_user.banner_file = banner_filename else: return redirect(url_for('profile_edit')) db.session.commit() return redirect(url_for('profile', username=current_user.username)) elif section == 'username': new_username = request.form.get('new_username') if new_username and new_username != current_user.username: if len(new_username) < 3 or len(new_username) > 20: pass elif User.query.filter_by(username=new_username).first(): pass else: old_username = current_user.username current_user.username = new_username update_related_tables(old_username, new_username) return redirect(url_for('profile', username=new_username)) elif section == 'bio': bio = request.form.get('bio') current_user.bio = bio db.session.commit() elif section == 'password': current_password = request.form.get('current_password') new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_new_password') if not current_user.check_password(current_password): pass elif new_password != confirm_password: pass elif len(new_password) < 6: pass else: current_user.encrypted_password = bcrypt.generate_password_hash(new_password).decode('utf-8') db.session.commit() return redirect(url_for('profile_edit')) elif section == 'create_post': post_text = request.form.get('post_text') post_media = request.files.get('post_media') if post_text: new_post = Post( username=current_user.username, text=post_text ) db.session.add(new_post) if post_media and allowed_file(post_media.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): if check_file_size(post_media, app.config['MAX_IMAGE_SIZE']): media_filename = secure_filename(post_media.filename) media_path = os.path.join(app.config['UPLOAD_FOLDER']['posts'], media_filename) post_media.save(media_path) new_post.media_file = media_filename db.session.commit() pass else: return redirect(url_for('profile_edit')) db.session.commit() return redirect(url_for('user_posts', username=current_user.username)) elif section == 'decoration': selected_item_id = request.form.get('selected_item') if selected_item_id: selected_item = UserItem.query.filter_by(username=current_user.username, item_id=selected_item_id).first() if selected_item: current_user.current_item = selected_item.item.id db.session.commit() pass else: pass return redirect(url_for('profile', username=current_user.username)) db.session.commit() return redirect(url_for('profile_edit')) return render_template( 'profile_edit.html', user=current_user, user_items=UserItem.query.filter_by(username=current_user.username).all() ) @app.route('/profile/') def profile(username): user = User.query.filter_by(username=username).first_or_404() if not user.avatar_file: user.avatar_file = 'default-avatar.png' if not user.banner_file: user.banner_file = 'default-banner.png' total_arts = Image.query.filter_by(username=username).count() total_videos = Video.query.filter_by(username=username).count() total_comics = Comic.query.filter_by(username=username).count() arts = Image.query.filter_by(username=username).order_by(Image.cookie_votes.desc(), Image.publication_date.desc()).limit(3).all() videos = Video.query.filter_by(username=username).order_by(Video.cookie_votes.desc(), Video.publication_date.desc()).limit(3).all() comics = Comic.query.filter_by(username=username).order_by(Comic.cookie_votes.desc(), Comic.publication_date.desc()).limit(3).all() subscribed = ( current_user.is_authenticated and Subscription.query.filter_by(user_id=current_user.id, author_id=user.id).first() is not None ) current_item = Item.query.get(user.current_item) if user.current_item else None is_current_user = current_user.is_authenticated and current_user.username == user.username subscriptions_count = Subscription.query.filter_by(author_id=user.id).count() return render_template( 'profile.html', user=user, arts=arts, videos=videos, comics=comics, total_arts=total_arts, total_videos=total_videos, total_comics=total_comics, current_item=current_item, is_current_user=is_current_user, subscribed=subscribed, subscriptions_count=subscriptions_count ) @app.route('/posts/') def user_posts(username): user = User.query.filter_by(username=username).first_or_404() posts = Post.query.filter_by(username=username).order_by(Post.post_date.desc()).all() avatars = {user.username: user.avatar_file for user in User.query.all()} return render_template('post.html', username=username, posts=posts, avatars=avatars) @app.route('/posts//add_comment/', methods=['POST']) @login_required def add_comment(username, post_id): comment_text = request.form.get('comment_text') if not comment_text: return redirect(url_for('user_posts', username=username)) comment = Comments( username=current_user.username, post_id=post_id, comment_text=comment_text, comment_date=datetime.utcnow() ) db.session.add(comment) db.session.commit() return redirect(url_for('user_posts', username=username)) @app.route('/subscribe/', methods=['POST']) @login_required def subscribe(author_id): if author_id == current_user.id: return redirect(url_for('profile', username=current_user.username)) existing_subscription = Subscription.query.filter_by(user_id=current_user.id, author_id=author_id).first() if existing_subscription: return redirect(url_for('profile', username=current_user.username)) new_subscription = Subscription(user_id=current_user.id, author_id=author_id) db.session.add(new_subscription) db.session.commit() return redirect(url_for('profile', username=current_user.username)) @app.route('/unsubscribe/', methods=['POST']) @login_required def unsubscribe(author_id): subscription = Subscription.query.filter_by( user_id=current_user.id, author_id=author_id).first() if subscription: db.session.delete(subscription) db.session.commit() return redirect(url_for('profile', username=User.query.get(author_id).username)) @app.route('/delete_post/', methods=['POST']) @login_required def delete_post(post_id): try: validate_csrf(request.form.get('csrf_token')) except BadRequest: return redirect(url_for('user_posts', username=current_user.username)) post = Post.query.get_or_404(post_id) if post.username != current_user.username: abort(403) if post.media_file: media_path = os.path.join(app.config['UPLOAD_FOLDER']['posts'], post.media_file) if os.path.exists(media_path): os.remove(media_path) db.session.delete(post) db.session.commit() return redirect(url_for('user_posts', username=current_user.username)) @app.route('/privacy_policy') def privacy_policy(): return render_template('privacy_policy.html') @app.route('/terms_of_use') def terms_of_use(): return render_template('terms_of_use.html') @app.route('/publication_rules') def publication_rules(): return render_template('publication_rules.html') @app.route('/shop') @login_required def shop(): items = Item.query.filter_by(visible=True).all() user_cookies = Cookies.query.filter_by(username=current_user.username).first().cookies if Cookies.query.filter_by(username=current_user.username).first() else 0 user_item_ids = {ui.item_id for ui in UserItem.query.filter_by(username=current_user.username).all()} return render_template('shop.html', items=items, user=current_user, user_cookies=user_cookies, user_item_ids=user_item_ids) @app.route('/buy_item/', methods=['POST']) @login_required def buy_item(item_id): username = current_user.username user_cookies = Cookies.query.filter_by(username=username).first() item = Item.query.get(item_id) if not user_cookies or not item or not item.visible or user_cookies.cookies < item.price: return redirect(url_for('shop')) if UserItem.query.filter_by(username=username, item_id=item.id).first(): return redirect(url_for('shop')) user_cookies.cookies -= item.price db.session.add(UserItem(username=username, item_id=item.id)) db.session.commit() return redirect(url_for('shop')) if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=True)