From a742f08f6d8ad4d8b8a0f9fb4e6669510205a33d Mon Sep 17 00:00:00 2001 From: Jeremy Zhang Date: Fri, 17 Aug 2018 03:12:21 +0000 Subject: [PATCH] Implement file uploading from the embeds --- ..._add_file_upload_column_to_guilds_table.py | 28 ++++ webapp/titanembeds/app.py | 1 + webapp/titanembeds/blueprints/admin/admin.py | 7 +- webapp/titanembeds/blueprints/api/api.py | 13 +- webapp/titanembeds/blueprints/user/user.py | 7 +- webapp/titanembeds/database/guilds.py | 1 + webapp/titanembeds/discordrest.py | 16 +- webapp/titanembeds/static/css/embed.css | 55 ++++++- .../static/js/administrate_guild.js | 9 ++ webapp/titanembeds/static/js/embed.js | 143 +++++++++++++++++- .../templates/administrate_guild.html.j2 | 13 ++ webapp/titanembeds/templates/embed.html.j2 | 26 ++++ webapp/titanembeds/utils.py | 13 +- 13 files changed, 306 insertions(+), 26 deletions(-) create mode 100644 webapp/alembic/versions/ce2b9c930a7a_add_file_upload_column_to_guilds_table.py diff --git a/webapp/alembic/versions/ce2b9c930a7a_add_file_upload_column_to_guilds_table.py b/webapp/alembic/versions/ce2b9c930a7a_add_file_upload_column_to_guilds_table.py new file mode 100644 index 0000000..8f91abc --- /dev/null +++ b/webapp/alembic/versions/ce2b9c930a7a_add_file_upload_column_to_guilds_table.py @@ -0,0 +1,28 @@ +"""Add file upload column to guilds table + +Revision ID: ce2b9c930a7a +Revises: 12267ce662e9 +Create Date: 2018-08-16 21:20:09.103071 + +""" + +# revision identifiers, used by Alembic. +revision = 'ce2b9c930a7a' +down_revision = '12267ce662e9' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('guilds', sa.Column('file_upload', sa.Boolean(), server_default='0', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('guilds', 'file_upload') + # ### end Alembic commands ### diff --git a/webapp/titanembeds/app.py b/webapp/titanembeds/app.py index fa9291f..465f2bf 100644 --- a/webapp/titanembeds/app.py +++ b/webapp/titanembeds/app.py @@ -32,6 +32,7 @@ app.config['SQLALCHEMY_POOL_SIZE'] = 15 app.config['RATELIMIT_STORAGE_URL'] = config["redis-uri"] app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=3) app.config['REDIS_URL'] = config["redis-uri"] +app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # Limit upload size to 4mb app.secret_key = config['app-secret'] sentry.init_app(app) diff --git a/webapp/titanembeds/blueprints/admin/admin.py b/webapp/titanembeds/blueprints/admin/admin.py index 6aa9180..40c4d58 100644 --- a/webapp/titanembeds/blueprints/admin/admin.py +++ b/webapp/titanembeds/blueprints/admin/admin.py @@ -193,7 +193,8 @@ def administrate_guild(guild_id): "banned_words_global_included": db_guild.banned_words_global_included, "banned_words": json.loads(db_guild.banned_words), "autorole_unauth": db_guild.autorole_unauth, - "autorole_discord": db_guild.autorole_discord + "autorole_discord": db_guild.autorole_discord, + "file_upload": db_guild.file_upload, } return render_template("administrate_guild.html.j2", guild=dbguild_dict, members=users, permissions=permissions, cosmetics=cosmetics) @@ -214,6 +215,7 @@ def update_administrate_guild(guild_id): db_guild.banned_words_global_included = request.form.get("banned_words_global_included", db_guild.banned_words_global_included) in ["true", True] db_guild.autorole_unauth = request.form.get("autorole_unauth", db_guild.autorole_unauth, type=int) db_guild.autorole_discord = request.form.get("autorole_discord", db_guild.autorole_discord, type=int) + db_guild.file_upload = request.form.get("file_upload", db_guild.file_upload) in ["true", True] invite_link = request.form.get("invite_link", db_guild.invite_link) if invite_link != None and invite_link.strip() == "": invite_link = None @@ -250,7 +252,8 @@ def update_administrate_guild(guild_id): banned_words_global_included=db_guild.banned_words_global_included, banned_words=json.loads(db_guild.banned_words), autorole_unauth=db_guild.autorole_unauth, - autorole_discord=db_guild.autorole_discord + autorole_discord=db_guild.autorole_discord, + file_upload=db_guild.file_upload, ) @admin.route("/guilds") diff --git a/webapp/titanembeds/blueprints/api/api.py b/webapp/titanembeds/blueprints/api/api.py index 0beb95e..5e681fd 100644 --- a/webapp/titanembeds/blueprints/api/api.py +++ b/webapp/titanembeds/blueprints/api/api.py @@ -273,7 +273,12 @@ def get_post_content_max_len(guild_id): def post(): guild_id = request.form.get("guild_id") channel_id = request.form.get('channel_id') - content = request.form.get('content') + content = request.form.get('content', "") + file = None + if "file" in request.files: + file = request.files["file"] + if file and file.filename == "": + file = None if "user_id" in session: dbUser = redisqueue.get_guild_member(guild_id, session["user_id"]) else: @@ -293,6 +298,8 @@ def post(): chan = filter_guild_channel(guild_id, channel_id) if not chan.get("write") or chan["channel"]["type"] != "text": status_code = 401 + elif file and not chan.get("attach_files"): + status_code = 406 elif not illegal_post: userid = session["user_id"] content = format_everyone_mention(chan, content) @@ -318,9 +325,9 @@ def post(): # username = "(Titan Dev) " + username username = username + "#" + str(session['discriminator']) avatar = session['avatar'] - message = discord_api.execute_webhook(webhook.get("id"), webhook.get("token"), username, avatar, content) + message = discord_api.execute_webhook(webhook.get("id"), webhook.get("token"), username, avatar, content, file) else: - message = discord_api.create_message(channel_id, content) + message = discord_api.create_message(channel_id, content, file) status_code = message['code'] db.session.commit() response = jsonify(message=message.get('content', message), status=status, illegal_reasons=illegal_reasons) diff --git a/webapp/titanembeds/blueprints/user/user.py b/webapp/titanembeds/blueprints/user/user.py index 7ead457..ec4f1af 100644 --- a/webapp/titanembeds/blueprints/user/user.py +++ b/webapp/titanembeds/blueprints/user/user.py @@ -230,7 +230,8 @@ def administrate_guild(guild_id): "banned_words_global_included": db_guild.banned_words_global_included, "banned_words": json.loads(db_guild.banned_words), "autorole_unauth": db_guild.autorole_unauth, - "autorole_discord": db_guild.autorole_discord + "autorole_discord": db_guild.autorole_discord, + "file_upload": db_guild.file_upload, } return render_template("administrate_guild.html.j2", guild=dbguild_dict, members=users, permissions=permissions, cosmetics=cosmetics, disabled=(guild_id in list_disabled_guilds())) @@ -259,6 +260,7 @@ def update_administrate_guild(guild_id): db_guild.banned_words_global_included = request.form.get("banned_words_global_included", db_guild.banned_words_global_included) in ["true", True] db_guild.autorole_unauth = request.form.get("autorole_unauth", db_guild.autorole_unauth, type=int) db_guild.autorole_discord = request.form.get("autorole_discord", db_guild.autorole_discord, type=int) + db_guild.file_upload = request.form.get("file_upload", db_guild.file_upload) in ["true", True] invite_link = request.form.get("invite_link", db_guild.invite_link) if invite_link != None and invite_link.strip() == "": @@ -299,7 +301,8 @@ def update_administrate_guild(guild_id): banned_words_global_included=db_guild.banned_words_global_included, banned_words=json.loads(db_guild.banned_words), autorole_unauth=db_guild.autorole_unauth, - autorole_discord=db_guild.autorole_discord + autorole_discord=db_guild.autorole_discord, + file_upload=db_guild.file_upload, ) @user.route("/add-bot/") diff --git a/webapp/titanembeds/database/guilds.py b/webapp/titanembeds/database/guilds.py index 132db23..7a2d3f8 100644 --- a/webapp/titanembeds/database/guilds.py +++ b/webapp/titanembeds/database/guilds.py @@ -19,6 +19,7 @@ class Guilds(db.Model): banned_words = db.Column(db.Text(), nullable=False, server_default="[]") # JSON list of strings to block from sending autorole_unauth = db.Column(db.BigInteger, nullable=True, server_default=None) # Automatic Role inherit for unauthenticated users autorole_discord = db.Column(db.BigInteger, nullable=True, server_default=None) # Automatic Role inherit for discord users + file_upload = db.Column(db.Boolean(), nullable=False, server_default="0") # Allow file uploading for server def __init__(self, guild_id): self.guild_id = guild_id diff --git a/webapp/titanembeds/discordrest.py b/webapp/titanembeds/discordrest.py index 08a32cf..4630c2e 100644 --- a/webapp/titanembeds/discordrest.py +++ b/webapp/titanembeds/discordrest.py @@ -2,6 +2,7 @@ import requests import sys import time import json +import urllib from titanembeds.utils import redis_store from flask import request @@ -61,7 +62,12 @@ class DiscordREST: time.sleep(int(self._get_bucket(url)) - curepoch) url_formatted = _DISCORD_API_BASE + url - req = requests.request(verb, url_formatted, params=params, data=data, headers=headers) + if data and "payload_json" in data: + if "Content-Type" in headers: + del headers["Content-Type"] + req = requests.request(verb, url_formatted, params=params, files=data, headers=headers) + else: + req = requests.request(verb, url_formatted, params=params, data=data, headers=headers) remaining = None if 'X-RateLimit-Remaining' in req.headers: @@ -103,9 +109,11 @@ class DiscordREST: # Channel ##################### - def create_message(self, channel_id, content): + def create_message(self, channel_id, content, file=None): _endpoint = "/channels/{channel_id}/messages".format(channel_id=channel_id) payload = {'content': content} + if file: + payload = {"payload_json": (None, json.dumps(payload)), "file": (file.filename, file.read(), 'application/octet-stream')} r = self.request("POST", _endpoint, data=payload) return r @@ -164,7 +172,7 @@ class DiscordREST: r = self.request("POST", _endpoint, data=payload, json=True) return r - def execute_webhook(self, webhook_id, webhook_token, username, avatar, content, wait=True): + def execute_webhook(self, webhook_id, webhook_token, username, avatar, content, file=None, wait=True): _endpoint = "/webhooks/{id}/{token}".format(id=webhook_id, token=webhook_token) if wait: _endpoint += "?wait=true" @@ -173,6 +181,8 @@ class DiscordREST: 'avatar_url': avatar, 'username': username } + if file: + payload = {"payload_json": (None, json.dumps(payload)), "file": (file.filename, file.read(), 'application/octet-stream')} r = self.request("POST", _endpoint, data=payload) return r diff --git a/webapp/titanembeds/static/css/embed.css b/webapp/titanembeds/static/css/embed.css index e574fb7..b6cf1bd 100644 --- a/webapp/titanembeds/static/css/embed.css +++ b/webapp/titanembeds/static/css/embed.css @@ -873,16 +873,65 @@ p.mentioned span.chatmessage { padding: 2px; } -@media only screen and (max-device-width: 320px) { +@media only screen and (max-width: 320px) { .wdt-emoji-picker { display: none; } + + #upload-file-btn { + display: none; + } } -@media only screen and (min-device-width: 321px) { +@media only screen and (min-width: 321px) { .wdt-emoji-picker { display: block; } + + #upload-file-btn { + display: block; + } +} + +#upload-file-btn { + position: absolute; + bottom: 5px; + right: 38px; + color: gray; + padding: 1px; + transition: .3s ease-out; +} + +#upload-file-btn:hover { + color: white; +} + +#fileinput { + display: none; +} + +@media only screen and (min-width: 500px) { + #filemodal-body { + display: flex; + justify-content: space-evenly; + } + + #filemodal-right { + width: 75%; + } +} + +#filepreview { + max-width: 100px; + max-height: 100px; +} + +#messagebox-filemodal { + background-color: rgba(0, 0, 0, 0.07); +} + +#messagebox-filemodal::placeholder { + color: #90a4ae; } #mention-picker { @@ -934,7 +983,7 @@ p.mentioned span.chatmessage { margin-left: auto; } -@media only screen and (max-device-width: 320px) { +@media only screen and (max-width: 320px) { #mention-picker .realname { display: none; } diff --git a/webapp/titanembeds/static/js/administrate_guild.js b/webapp/titanembeds/static/js/administrate_guild.js index fb5c5f8..698bede 100644 --- a/webapp/titanembeds/static/js/administrate_guild.js +++ b/webapp/titanembeds/static/js/administrate_guild.js @@ -173,6 +173,15 @@ $("#autorole_discord").change(function () { }); }); +$('#file_upload').change(function() { + var pathname = window.location.pathname; + var checked = $(this).is(':checked') + var payload = {"file_upload": checked} + $.post(pathname, payload, function(data) { + Materialize.toast('Updated file uploads setting!', 2000) + }); +}); + function initiate_ban(guild_id, user_id) { var reason = prompt("Please enter your reason for ban"); var payload = { diff --git a/webapp/titanembeds/static/js/embed.js b/webapp/titanembeds/static/js/embed.js index 843ada7..2b25c18 100644 --- a/webapp/titanembeds/static/js/embed.js +++ b/webapp/titanembeds/static/js/embed.js @@ -137,13 +137,44 @@ return funct.promise(); } - function post(channel_id, content) { - var funct = $.ajax({ + function post(channel_id, content, file) { + if (content == "") { + content = null; + } + var data = null; + var ajaxobj = { method: "POST", dataType: "json", - url: "/api/post", - data: {"guild_id": guild_id, "channel_id": channel_id, "content": content} - }); + url: "/api/post" + } + if (file) { + data = new FormData(); + data.append("guild_id", guild_id); + data.append("channel_id", channel_id); + if (content) { + data.append("content", content); + } + data.append("file", file); + ajaxobj.cache = false; + ajaxobj.contentType = false; + ajaxobj.processData = false; + ajaxobj.xhr = function() { + var myXhr = $.ajaxSettings.xhr(); + if (myXhr.upload) { + // For handling the progress of the upload + myXhr.upload.addEventListener('progress', function(e) { + if (e.lengthComputable) { + $("#filemodalprogress-inner").css("width", (e.loaded/e.total) + "%") + } + } , false); + } + return myXhr; + } + } else { + data = {"guild_id": guild_id, "channel_id": channel_id, "content": content}; + } + ajaxobj.data = data; + var funct = $.ajax(ajaxobj); return funct.promise(); } @@ -214,6 +245,13 @@ inDuration: 400, outDuration: 400, }); + $("#filemodal").modal({ + dismissible: true, + opacity: .3, + inDuration: 400, + outDuration: 400, + complete: function () { $("#fileinput").val(""); } + }); $("#usercard").modal({ opacity: .5, }); @@ -237,6 +275,44 @@ $("#nsfwmodal").modal("close"); }); + $("#upload-file-btn").click(function () { + $("#fileinput").trigger('click'); + }); + + $("#proceed_fileupload_btn").click(function () { + $("#messagebox-filemodal").trigger(jQuery.Event("keydown", { keyCode: 13 } )); + }); + + $("#fileinput").change(function (e){ + var files = e.target.files; + if (files && files.length > 0) { + $("#messagebox-filemodal").val($("#messagebox").val()); + $("#filename").text($("#fileinput")[0].files[0].name); + $("#filemodal").modal("open"); + $("#messagebox-filemodal").focus(); + var file = files[0]; + var file_size = file.size; + var file_max_size = 4 * 1024 * 1024; + if (file_size > file_max_size) { + $("#filemodal").modal("close"); + Materialize.toast('Your file is too powerful! The maximum file size is 4 megabytes.', 5000); + return; + } + var name = file.name; + var extension = name.substr(-4).toLowerCase(); + var image_extensions = [".png", ".jpg", ".jpeg", ".gif"]; + $("#filepreview").hide(); + if (FileReader && image_extensions.indexOf(extension) > -1) { + var reader = new FileReader(); + reader.onload = function() { + $("#filepreview").show(); + $("#filepreview")[0].src = reader.result; + }; + reader.readAsDataURL(file); + } + } + }); + $( "#theme-selector" ).change(function () { var theme = $("#theme-selector option:selected").val(); var keep_custom_css = $("#overwrite_theme_custom_css_checkbox").is(':checked'); @@ -444,11 +520,13 @@ $("#messagebox").hide(); $("#emoji-tray-toggle").hide(); $(".wdt-emoji-picker").hide(); + $("#upload-file-btn").hide(); } else { $("#visitor_mode_message").hide(); $("#messagebox").show(); $("#emoji-tray-toggle").show(); $(".wdt-emoji-picker").show(); + $("#upload-file-btn").show(); } } @@ -658,6 +736,7 @@ if (curr_default_channel == null) { $("#messagebox").prop('disabled', true); $("#messagebox").prop('placeholder', "NO TEXT CHANNELS"); + $("#upload-file-btn").hide(); Materialize.toast("You find yourself in a strange place. You don't have access to any text channels, or there are none in this server.", 20000); return; } @@ -667,9 +746,18 @@ if (this_channel.write) { $("#messagebox").prop('disabled', false); $("#messagebox").prop('placeholder', "Enter message"); + $("#upload-file-btn").show(); + $(".wdt-emoji-picker").show(); } else { $("#messagebox").prop('disabled', true); $("#messagebox").prop('placeholder', "Messages is disabled in this channel."); + $("#upload-file-btn").hide(); + $(".wdt-emoji-picker").hide(); + } + if (this_channel.attach_files) { + $("#upload-file-btn").show(); + } else { + $("#upload-file-btn").hide(); } $("#channeltopic").text(this_channel.channel.topic); $("#channel-"+selected_channel).parent().addClass("active"); @@ -1744,9 +1832,11 @@ if (event.keyCode == 16) { shift_pressed = true; } - if(event.keyCode == 13 && !shift_pressed && $(this).val().length >= 1) { + if(event.keyCode == 13 && !shift_pressed && ($(this).val().length >= 1 || $("#fileinput").val().length >= 1)) { $(this).val($.trim($(this).val())); $(this).blur(); + $("#messagebox-filemodal").attr('readonly', true); + $("#proceed_fileupload_btn").attr("disabled", true); $("#messagebox").attr('readonly', true); var emojiConvertor = new EmojiConvertor(); emojiConvertor.init_env(); @@ -1754,9 +1844,18 @@ emojiConvertor.allow_native = true; var messageInput = emojiConvertor.replace_colons($(this).val()); messageInput = stringToDefaultEmote(messageInput); - var funct = post(selected_channel, messageInput); + var file = null; + if ($("#fileinput")[0].files.length > 0) { + file = $("#fileinput")[0].files[0]; + } + $("#filemodalprogress").show(); + var funct = post(selected_channel, messageInput, file); funct.done(function(data) { $("#messagebox").val(""); + $("#messagebox-filemodal").val(""); + $("#fileinput").val(""); + $("#filemodal").modal("close"); + $("#filemodalprogress").hide(); }); funct.fail(function(data) { Materialize.toast('Failed to send message.', 10000); @@ -1773,10 +1872,38 @@ }); funct.always(function() { $("#messagebox").attr('readonly', false); - $("#messagebox").focus(); + $("#messagebox-filemodal").attr('readonly', false); + $("#proceed_fileupload_btn").attr("disabled", false); + if ($("#filemodal").is(":visible")) { + $("#messagebox-filemodal").focus(); + } else { + $("#messagebox").focus(); + } }); } }); + + $("#messagebox-filemodal").keyup(function (event) { + if (event.keyCode == 16) { + shift_pressed = false; + } + }); + + $("#messagebox-filemodal").keydown(function (event) { + if ($(this).val().length == 1) { + $(this).val($.trim($(this).val())); + } + if (event.keyCode == 16) { + shift_pressed = true; + } + + if(event.keyCode == 13 && !shift_pressed) { + $(this).val($.trim($(this).val())); + $(this).blur(); + $("#messagebox").val($(this).val()); + $("#messagebox").trigger(jQuery.Event("keydown", { keyCode: 13 } )); + } + }); $('#guild-btn').sideNav({ menuWidth: 300, // Default is 300 diff --git a/webapp/titanembeds/templates/administrate_guild.html.j2 b/webapp/titanembeds/templates/administrate_guild.html.j2 index b1628c8..4aff035 100644 --- a/webapp/titanembeds/templates/administrate_guild.html.j2 +++ b/webapp/titanembeds/templates/administrate_guild.html.j2 @@ -214,6 +214,19 @@ +
+ +

Toggle File Attachments

+

Allow embed users to attach files to your Discord server in messages

+
+ +
+ diff --git a/webapp/titanembeds/templates/embed.html.j2 b/webapp/titanembeds/templates/embed.html.j2 index 82ab348..098f28f 100644 --- a/webapp/titanembeds/templates/embed.html.j2 +++ b/webapp/titanembeds/templates/embed.html.j2 @@ -79,6 +79,7 @@
+ file_upload
@@ -243,6 +244,31 @@ + +