diff --git a/files/__main__.py b/files/__main__.py index d59cdd0f52..de05558046 100644 --- a/files/__main__.py +++ b/files/__main__.py @@ -76,6 +76,9 @@ app.config["SPAM_URL_SIMILARITY_THRESHOLD"] = float(environ.get("SPAM_URL_SIMILA app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"] = float(environ.get("COMMENT_SPAM_SIMILAR_THRESHOLD", 0.5)) app.config["COMMENT_SPAM_COUNT_THRESHOLD"] = int(environ.get("COMMENT_SPAM_COUNT_THRESHOLD", 0.5)) +# how many coins are required to upload videos +app.config["VIDEO_COIN_REQUIREMENT"] = int(environ.get("VIDEO_COIN_REQUIREMENT", 0)) + app.config["CACHE_REDIS_URL"] = environ.get("REDIS_URL").strip() app.config["CACHE_DEFAULT_TIMEOUT"] = 60 app.config["CACHE_KEY_PREFIX"] = "flask_caching_" diff --git a/files/classes/submission.py b/files/classes/submission.py index 83147990c3..c7bcc740b8 100644 --- a/files/classes/submission.py +++ b/files/classes/submission.py @@ -45,6 +45,7 @@ class Submission(Base, Stndrd, Age_times, Scores, Fuzzing): thumburl = Column(String) is_banned = Column(Boolean, default=False) bannedfor = Column(Boolean) + processing = Column(Boolean, default=False) views = Column(Integer, default=0) deleted_utc = Column(Integer, default=0) distinguish_level = Column(Integer, default=0) @@ -400,6 +401,13 @@ class Submission(Base, Stndrd, Age_times, Scores, Fuzzing): if self.url: return self.url.lower().endswith('.jpg') or self.url.lower().endswith('.png') or self.url.lower().endswith('.gif') or self.url.lower().endswith('.jpeg') or self.url.lower().endswith('?maxwidth=9999') else: return False + @property + def is_video(self) -> bool: + if self.url: + return self.url.startswith("https://i.imgur.com") and self.url.lower().endswith('.mp4') + else: + return False + @property @lazy def active_flags(self): return self.flags.count() diff --git a/files/helpers/alerts.py b/files/helpers/alerts.py index fe8f686bde..8b55d1737e 100644 --- a/files/helpers/alerts.py +++ b/files/helpers/alerts.py @@ -7,7 +7,16 @@ from .sanitize import * from .const import * -def send_notification(vid, user, text): +def send_notification(vid, user, text, db=None): + + # for when working outside request context + if isinstance(user, int): + uid = user + else: + uid = user.id + + if not db: + db = g.db text = text.replace('r/', 'r\/').replace('u/', 'u\/') text = text.replace("\n", "\n\n").replace("\n\n\n\n\n\n", "\n\n").replace("\n\n\n\n", "\n\n").replace("\n\n\n", "\n\n") @@ -20,19 +29,19 @@ def send_notification(vid, user, text): parent_submission=None, distinguish_level=6, ) - g.db.add(new_comment) + db.add(new_comment) - g.db.flush() + db.flush() new_aux = CommentAux(id=new_comment.id, body=text, body_html=text_html, ) - g.db.add(new_aux) + db.add(new_aux) notif = Notification(comment_id=new_comment.id, - user_id=user.id) - g.db.add(notif) + user_id=uid) + db.add(notif) def send_pm(vid, user, text): diff --git a/files/helpers/images.py b/files/helpers/images.py index 37c1914be8..6cd522f97a 100644 --- a/files/helpers/images.py +++ b/files/helpers/images.py @@ -1,9 +1,10 @@ import requests -from os import environ +from os import environ, path, remove from PIL import Image as IImage, ImageSequence import base64 from files.classes.images import * from flask import g +from werkzeug.utils import secure_filename CF_KEY = environ.get("CLOUDFLARE_KEY", "").strip() CF_ZONE = environ.get("CLOUDFLARE_ZONE", "").strip() @@ -86,4 +87,36 @@ def upload_imgur(file=None, resize=False, png=False): new_image = Image(text=url, deletehash=resp["deletehash"]) g.db.add(new_image) - return(url) \ No newline at end of file + return(url) + + +class UploadException(Exception): + """Custom exception to raise if upload goes wrong""" + pass + + +def upload_video(file): + + file_path = path.join("temp", secure_filename(file.filename)) + file.save(file_path) + + headers = {"Authorization": f"Client-ID {IMGUR_KEY}"} + with open(file_path, 'rb') as f: + try: + r = requests.post('https://api.imgur.com/3/upload', headers=headers, files={"video": f}) + + r.raise_for_status() + + resp = r.json()['data'] + except requests.HTTPError as e: + raise UploadException("Invalid video. Make sure it's 1 minute long or shorter.") + except Exception: + raise UploadException("Error, please try again later.") + finally: + remove(file_path) + + link = resp['link'] + img = Image(text=link, deletehash=resp['deletehash']) + g.db.add(img) + + return link diff --git a/files/routes/front.py b/files/routes/front.py index 88c76393e9..36832ea6bd 100644 --- a/files/routes/front.py +++ b/files/routes/front.py @@ -121,6 +121,11 @@ def frontlist(v=None, sort="hot", page=1, t="all", ids_only=True, filter_words=' posts = posts.filter_by(is_banned=False,stickied=False,private=False).filter(Submission.deleted_utc == 0) + if v: + posts = posts.filter(or_(Submission.processing == False, Submission.author_id == v.id)) + else: + posts = posts.filter_by(processing=False) + if v and v.admin_level == 0: blocking = g.db.query( UserBlock.target_id).filter_by( diff --git a/files/routes/posts.py b/files/routes/posts.py index d1ced7a92a..b07b62388c 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -1,3 +1,4 @@ +import time from urllib.parse import urlparse import mistletoe import urllib.parse @@ -527,6 +528,44 @@ def filter_title(title): return title + +IMGUR_KEY = environ.get("IMGUR_KEY", "").strip() + + +def check_processing_thread(v, post, link, db): + + image_id = link.split('/')[-1].rstrip('.mp4') + headers = {"Authorization": f"Client-ID {IMGUR_KEY}"} + + while True: + # break on error to prevent zombie threads + try: + time.sleep(15) + + req = requests.get(f"https://api.imgur.com/3/image/{image_id}", headers=headers) + + status = req.json()['data']['processing']['status'] + if status == 'completed': + post.processing = False + db.add(post) + + send_notification( + NOTIFICATIONS_ACCOUNT, + v, + f"Your video has finished processing and your [post](/post/{post.id}) is now live.", + db=db + ) + + db.commit() + break + # just in case + elif status == 'failed': + print(f"video upload for post {post.id} failed") + break + except Exception: + break + + @app.post("/submit") @limiter.limit("6/minute") @is_not_banned @@ -827,13 +866,72 @@ def submit_post(v): abort(413) file = request.files['file'] - if not file.content_type.startswith('image/'): - if request.headers.get("Authorization"): return {"error": f"Image files only"}, 400 - else: return render_template("submit.html", v=v, error=f"Image files only.", title=title, body=request.form.get("body", "")), 400 + #if not file.content_type.startswith('image/'): + # if request.headers.get("Authorization"): return {"error": f"Image files only"}, 400 + # else: return render_template("submit.html", v=v, error=f"Image files only.", title=title, body=request.form.get("body", "")), 400 + if not file.content_type.startswith(('image/', 'video/')): + if request.headers.get("Authorization"): return {"error": f"File type not allowed"}, 400 + else: return render_template("submit.html", v=v, error=f"File type not allowed.", title=title, body=request.form.get("body", "")), 400 - if 'pcm' in request.host: new_post.url = upload_ibb(file) - else: new_post.url = upload_imgur(file) + if file.content_type.startswith('video/') and v.coins < app.config["VIDEO_COIN_REQUIREMENT"] and v.admin_level < 1: + if request.headers.get("Authorization"): + return { + "error": f"You need at least {app.config['VIDEO_COIN_REQUIREMENT']} coins to upload videos" + }, 403 + else: + return render_template( + "submit.html", + v=v, + error=f"You need at least {app.config['VIDEO_COIN_REQUIREMENT']} coins to upload videos.", + title=title, + body=request.form.get("body", "") + ), 403 + + if 'pcm' in request.host: + if file.content_type.startswith('image/'): + new_post.url = upload_ibb(file) + else: + try: + post_url = upload_video(file) + new_post.url = post_url + new_post.processing = True + gevent.spawn(check_processing_thread, v.id, new_post, post_url, g.db) + except UploadException as e: + if request.headers.get("Authorization"): + return { + "error": str(e), + }, 400 + else: + return render_template( + "submit.html", + v=v, + error=str(e), + title=title, + body=request.form.get("body", "") + ), 400 + else: + if file.content_type.startswith('image/'): + new_post.url = upload_imgur(file) + else: + try: + post_url = upload_video(file) + new_post.url = post_url + new_post.processing = True + gevent.spawn(check_processing_thread, v.id, new_post, post_url, g.db) + except UploadException as e: + if request.headers.get("Authorization"): + return { + "error": str(e), + }, 400 + else: + return render_template( + "submit.html", + v=v, + error=str(e), + title=title, + body=request.form.get("body", "") + ), 400 g.db.add(new_post) g.db.add(new_post.submission_aux) diff --git a/files/templates/submission.html b/files/templates/submission.html index b1e4c2aed9..28acbe3dbd 100644 --- a/files/templates/submission.html +++ b/files/templates/submission.html @@ -36,6 +36,9 @@ +{% if p.is_video %} + +{% endif %} @@ -62,6 +65,9 @@ +{% if p.is_video %} + +{% endif %} @@ -256,6 +262,7 @@ {% if p.is_bot %} {% endif %} {% if p.over_18 %}+18{% endif %} {% if p.private %}Draft{% endif %} + {% if p.processing %}uploading...{% endif %} {% if p.active_flags %}{{p.active_flags}} Reports{% endif %} {% if p.author.verified %} {% endif %} @@ -301,7 +308,7 @@

 
-							{% elif not p.embed_url and not p.is_image %}
+							{% elif not p.embed_url and not p.is_image and not p.is_video %}
 							
 								
{{p.domain|truncate(30, True)}} @@ -321,6 +328,15 @@

+							{% elif p.is_video %}
+								
+
+ +
+
+

 							{% endif %}
 							{{p.realbody(v) | safe}}
 						
@@ -497,7 +513,7 @@
 
 		
 
-		{% if not p.is_image %}
+		{% if not p.is_image and not p.is_video %}
 			
diff --git a/files/templates/submission_listing.html b/files/templates/submission_listing.html index f70ee6a4aa..897d6aaa74 100644 --- a/files/templates/submission_listing.html +++ b/files/templates/submission_listing.html @@ -69,10 +69,10 @@ {% elif p.is_image %} - + - {% else %} + {% elif not p.is_video %} @@ -104,6 +104,7 @@ {% if p.is_blocking %}{% endif %} {% if p.is_blocked %}{% endif %} {% if p.private %}Draft{% endif %} + {% if p.processing %}uploading...{% endif %} {% if p.active_flags %}{{p.active_flags}} Reports{% endif %} {% if p.author.verified %} {% endif %} @@ -392,6 +393,14 @@ Unable to load image
+{% elif p.is_video %} + +
+ +
+
{% elif p.embed_url and "youtu" in p.domain or "streamable.com/" in p.url %}
diff --git a/files/templates/submit.html b/files/templates/submit.html index a40655f273..5a11d150c2 100644 --- a/files/templates/submit.html +++ b/files/templates/submit.html @@ -330,16 +330,17 @@
-
+
- Images uploaded will be public. Optional if you have text. + Optional if you have text. + You can upload videos up to 1 minute long if you have at least {{ 'VIDEO_COIN_REQUIREMENT' | app_config }} {{ 'COINS_NAME' | app_config }}{% if v.admin_level > 1 %} or are an admin{% endif %}.