Refactored the Image Uploader feature. (#4971)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Mm2PL 2023-11-19 12:05:30 +01:00 committed by GitHub
parent 7898b97fc2
commit fbc8aacabe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 266 additions and 139 deletions

View file

@ -39,6 +39,8 @@
- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965) - Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965)
- Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965) - Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965)
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965) - Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965)
- Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971)
- Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971)
- Dev: Change clang-format from v14 to v16. (#4929) - Dev: Change clang-format from v14 to v16. (#4929)
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
- Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767) - Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767)
@ -66,6 +68,7 @@
- Dev: `Details` file properties tab is now populated on Windows. (#4912) - Dev: `Details` file properties tab is now populated on Windows. (#4912)
- Dev: Removed `Outcome` from network requests. (#4959) - Dev: Removed `Outcome` from network requests. (#4959)
- Dev: Added Tests for Windows and MacOS in CI. (#4970) - Dev: Added Tests for Windows and MacOS in CI. (#4970)
- Dev: Refactored the Image Uploader feature. (#4971)
## 2.4.6 ## 2.4.6

View file

@ -89,6 +89,11 @@ public:
{ {
return nullptr; return nullptr;
} }
ImageUploader *getImageUploader() override
{
return nullptr;
}
}; };
} // namespace chatterino::mock } // namespace chatterino::mock

View file

@ -10,6 +10,7 @@
#include "controllers/hotkeys/HotkeyController.hpp" #include "controllers/hotkeys/HotkeyController.hpp"
#include "controllers/ignores/IgnoreController.hpp" #include "controllers/ignores/IgnoreController.hpp"
#include "controllers/notifications/NotificationController.hpp" #include "controllers/notifications/NotificationController.hpp"
#include "singletons/ImageUploader.hpp"
#ifdef CHATTERINO_HAVE_PLUGINS #ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/PluginController.hpp" # include "controllers/plugins/PluginController.hpp"
#endif #endif
@ -79,6 +80,7 @@ Application::Application(Settings &_settings, Paths &_paths)
, hotkeys(&this->emplace<HotkeyController>()) , hotkeys(&this->emplace<HotkeyController>())
, windows(&this->emplace<WindowManager>()) , windows(&this->emplace<WindowManager>())
, toasts(&this->emplace<Toasts>()) , toasts(&this->emplace<Toasts>())
, imageUploader(&this->emplace<ImageUploader>())
, commands(&this->emplace<CommandController>()) , commands(&this->emplace<CommandController>())
, notifications(&this->emplace<NotificationController>()) , notifications(&this->emplace<NotificationController>())

View file

@ -41,6 +41,7 @@ class Toasts;
class ChatterinoBadges; class ChatterinoBadges;
class FfzBadges; class FfzBadges;
class SeventvBadges; class SeventvBadges;
class ImageUploader;
class IApplication class IApplication
{ {
@ -66,6 +67,7 @@ public:
virtual SeventvBadges *getSeventvBadges() = 0; virtual SeventvBadges *getSeventvBadges() = 0;
virtual IUserDataController *getUserData() = 0; virtual IUserDataController *getUserData() = 0;
virtual ITwitchLiveController *getTwitchLiveController() = 0; virtual ITwitchLiveController *getTwitchLiveController() = 0;
virtual ImageUploader *getImageUploader() = 0;
}; };
class Application : public IApplication class Application : public IApplication
@ -94,6 +96,7 @@ public:
HotkeyController *const hotkeys{}; HotkeyController *const hotkeys{};
WindowManager *const windows{}; WindowManager *const windows{};
Toasts *const toasts{}; Toasts *const toasts{};
ImageUploader *const imageUploader{};
CommandController *const commands{}; CommandController *const commands{};
NotificationController *const notifications{}; NotificationController *const notifications{};
@ -167,6 +170,10 @@ public:
} }
IUserDataController *getUserData() override; IUserDataController *getUserData() override;
ITwitchLiveController *getTwitchLiveController() override; ITwitchLiveController *getTwitchLiveController() override;
ImageUploader *getImageUploader() override
{
return this->imageUploader;
}
pajlada::Signals::NoArgSignal streamerModeChanged; pajlada::Signals::NoArgSignal streamerModeChanged;

View file

@ -424,6 +424,8 @@ set(SOURCE_FILES
singletons/Emotes.hpp singletons/Emotes.hpp
singletons/Fonts.cpp singletons/Fonts.cpp
singletons/Fonts.hpp singletons/Fonts.hpp
singletons/ImageUploader.cpp
singletons/ImageUploader.hpp
singletons/Logging.cpp singletons/Logging.cpp
singletons/Logging.hpp singletons/Logging.hpp
singletons/NativeMessaging.cpp singletons/NativeMessaging.cpp
@ -475,8 +477,6 @@ set(SOURCE_FILES
util/IpcQueue.hpp util/IpcQueue.hpp
util/LayoutHelper.cpp util/LayoutHelper.cpp
util/LayoutHelper.hpp util/LayoutHelper.hpp
util/NuulsUploader.cpp
util/NuulsUploader.hpp
util/RapidjsonHelpers.cpp util/RapidjsonHelpers.cpp
util/RapidjsonHelpers.hpp util/RapidjsonHelpers.hpp
util/RatelimitBucket.cpp util/RatelimitBucket.cpp

View file

@ -32,7 +32,7 @@ Q_LOGGING_CATEGORY(chatterinoNativeMessage, "chatterino.nativemessage",
Q_LOGGING_CATEGORY(chatterinoNetwork, "chatterino.network", logThreshold); Q_LOGGING_CATEGORY(chatterinoNetwork, "chatterino.network", logThreshold);
Q_LOGGING_CATEGORY(chatterinoNotification, "chatterino.notification", Q_LOGGING_CATEGORY(chatterinoNotification, "chatterino.notification",
logThreshold); logThreshold);
Q_LOGGING_CATEGORY(chatterinoNuulsuploader, "chatterino.nuulsuploader", Q_LOGGING_CATEGORY(chatterinoImageuploader, "chatterino.imageuploader",
logThreshold); logThreshold);
Q_LOGGING_CATEGORY(chatterinoPubSub, "chatterino.pubsub", logThreshold); Q_LOGGING_CATEGORY(chatterinoPubSub, "chatterino.pubsub", logThreshold);
Q_LOGGING_CATEGORY(chatterinoRecentMessages, "chatterino.recentmessages", Q_LOGGING_CATEGORY(chatterinoRecentMessages, "chatterino.recentmessages",

View file

@ -25,7 +25,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoMessage);
Q_DECLARE_LOGGING_CATEGORY(chatterinoNativeMessage); Q_DECLARE_LOGGING_CATEGORY(chatterinoNativeMessage);
Q_DECLARE_LOGGING_CATEGORY(chatterinoNetwork); Q_DECLARE_LOGGING_CATEGORY(chatterinoNetwork);
Q_DECLARE_LOGGING_CATEGORY(chatterinoNotification); Q_DECLARE_LOGGING_CATEGORY(chatterinoNotification);
Q_DECLARE_LOGGING_CATEGORY(chatterinoNuulsuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoImageuploader);
Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub); Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub);
Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages); Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages);
Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings); Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings);

View file

@ -6,6 +6,7 @@
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "messages/Image.hpp" #include "messages/Image.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageColor.hpp"
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
#include "providers/LinkResolver.hpp" #include "providers/LinkResolver.hpp"
#include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubActions.hpp"
@ -660,6 +661,58 @@ MessageBuilder::MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag /*unused*/,
this->message().flags.set(MessageFlag::DoNotTriggerNotification); this->message().flags.set(MessageFlag::DoNotTriggerNotification);
} }
MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/,
const QString &imageLink,
const QString &deletionLink,
size_t imagesStillQueued, size_t secondsLeft)
: MessageBuilder()
{
this->message().flags.set(MessageFlag::System);
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
this->emplace<TimestampElement>();
using MEF = MessageElementFlag;
auto addText = [this](QString text, MessageElementFlags mefs = MEF::Text,
MessageColor color =
MessageColor::System) -> TextElement * {
this->message().searchText += text;
this->message().messageText += text;
return this->emplace<TextElement>(text, mefs, color);
};
addText("Your image has been uploaded to");
// ASSUMPTION: the user gave this uploader configuration to the program
// therefore they trust that the host is not wrong/malicious. This doesn't obey getSettings()->lowercaseDomains.
// This also ensures that the LinkResolver doesn't get these links.
addText(imageLink, {MEF::OriginalLink, MEF::LowercaseLink},
MessageColor::Link)
->setLink({Link::Url, imageLink})
->setTrailingSpace(false);
if (!deletionLink.isEmpty())
{
addText("(Deletion link:");
addText(deletionLink, {MEF::OriginalLink, MEF::LowercaseLink},
MessageColor::Link)
->setLink({Link::Url, deletionLink})
->setTrailingSpace(false);
addText(")")->setTrailingSpace(false);
}
addText(".");
if (imagesStillQueued == 0)
{
return;
}
addText(QString("%1 left. Please wait until all of them are uploaded. "
"About %2 seconds left.")
.arg(imagesStillQueued)
.arg(secondsLeft));
}
Message *MessageBuilder::operator->() Message *MessageBuilder::operator->()
{ {
return this->message_.get(); return this->message_.get();

View file

@ -37,6 +37,9 @@ struct LiveUpdatesAddEmoteMessageTag {
}; };
struct LiveUpdatesUpdateEmoteSetMessageTag { struct LiveUpdatesUpdateEmoteSetMessageTag {
}; };
struct ImageUploaderResultTag {
};
const SystemMessageTag systemMessage{}; const SystemMessageTag systemMessage{};
const TimeoutMessageTag timeoutMessage{}; const TimeoutMessageTag timeoutMessage{};
const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{}; const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{};
@ -44,6 +47,10 @@ const LiveUpdatesRemoveEmoteMessageTag liveUpdatesRemoveEmoteMessage{};
const LiveUpdatesAddEmoteMessageTag liveUpdatesAddEmoteMessage{}; const LiveUpdatesAddEmoteMessageTag liveUpdatesAddEmoteMessage{};
const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{}; const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{};
// This signifies that you want to construct a message containing the result of
// a successful image upload.
const ImageUploaderResultTag imageUploaderResultMessage{};
MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text);
MessagePtr makeSystemMessage(const QString &text, const QTime &time); MessagePtr makeSystemMessage(const QString &text, const QTime &time);
std::pair<MessagePtr, MessagePtr> makeAutomodMessage( std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
@ -88,6 +95,16 @@ public:
MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag, const QString &platform, MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag, const QString &platform,
const QString &actor, const QString &emoteSetName); const QString &actor, const QString &emoteSetName);
/**
* "Your image has been uploaded to %1[ (Deletion link: %2)]."
* or "Your image has been uploaded to %1 %2. %3 left. "
* "Please wait until all of them are uploaded. "
* "About %4 seconds left."
*/
MessageBuilder(ImageUploaderResultTag, const QString &imageLink,
const QString &deletionLink, size_t imagesStillQueued = 0,
size_t secondsLeft = 0);
virtual ~MessageBuilder() = default; virtual ~MessageBuilder() = default;
Message *operator->(); Message *operator->();

View file

@ -1,9 +1,10 @@
#include "NuulsUploader.hpp" #include "singletons/ImageUploader.hpp"
#include "common/Env.hpp" #include "common/Env.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "common/NetworkResult.hpp" #include "common/NetworkResult.hpp"
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -16,6 +17,7 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QMutex> #include <QMutex>
#include <QPointer>
#include <QSaveFile> #include <QSaveFile>
#define UPLOAD_DELAY 2000 #define UPLOAD_DELAY 2000
@ -41,13 +43,10 @@ std::optional<QByteArray> convertToPng(const QImage &image)
namespace chatterino { namespace chatterino {
// These variables are only used from the main thread.
static auto uploadMutex = QMutex();
static std::queue<RawImageData> uploadQueue;
// logging information on successful uploads to a json file // logging information on successful uploads to a json file
void logToFile(const QString originalFilePath, QString imageLink, void ImageUploader::logToFile(const QString &originalFilePath,
QString deletionLink, ChannelPtr channel) const QString &imageLink,
const QString &deletionLink, ChannelPtr channel)
{ {
const QString logFileName = const QString logFileName =
combinePath((getSettings()->logPath.getValue().isEmpty() combinePath((getSettings()->logPath.getValue().isEmpty()
@ -120,8 +119,13 @@ QString getLinkFromResponse(NetworkResult response, QString pattern)
return pattern; return pattern;
} }
void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel, void ImageUploader::save()
ResizingTextEdit &textEdit) {
}
void ImageUploader::sendImageUploadRequest(RawImageData imageData,
ChannelPtr channel,
QPointer<ResizingTextEdit> textEdit)
{ {
const static char *const boundary = "thisistheboudaryasd"; const static char *const boundary = "thisistheboudaryasd";
const static QString contentType = const static QString contentType =
@ -155,91 +159,103 @@ void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel,
.header("Content-Type", contentType) .header("Content-Type", contentType)
.headerList(extraHeaders) .headerList(extraHeaders)
.multiPart(payload) .multiPart(payload)
.onSuccess([&textEdit, channel, .onSuccess(
originalFilePath](NetworkResult result) { [textEdit, channel, originalFilePath, this](NetworkResult result) {
QString link = getSettings()->imageUploaderLink.getValue().isEmpty() this->handleSuccessfulUpload(result, originalFilePath, channel,
? result.getData() textEdit);
: getLinkFromResponse( })
result, getSettings()->imageUploaderLink); .onError([channel, this](NetworkResult result) -> bool {
QString deletionLink = this->handleFailedUpload(result, channel);
getSettings()->imageUploaderDeletionLink.getValue().isEmpty()
? ""
: getLinkFromResponse(
result, getSettings()->imageUploaderDeletionLink);
qCDebug(chatterinoNuulsuploader) << link << deletionLink;
textEdit.insertPlainText(link + " ");
if (uploadQueue.empty())
{
channel->addMessage(makeSystemMessage(
QString("Your image has been uploaded to %1 %2.")
.arg(link)
.arg(deletionLink.isEmpty()
? ""
: QString("(Deletion link: %1 )")
.arg(deletionLink))));
uploadMutex.unlock();
}
else
{
channel->addMessage(makeSystemMessage(
QString("Your image has been uploaded to %1 %2. %3 left. "
"Please wait until all of them are uploaded. "
"About %4 seconds left.")
.arg(link)
.arg(deletionLink.isEmpty()
? ""
: QString("(Deletion link: %1 )")
.arg(deletionLink))
.arg(uploadQueue.size())
.arg(uploadQueue.size() * (UPLOAD_DELAY / 1000 + 1))));
// 2 seconds for the timer that's there not to spam the remote server
// and 1 second of actual uploading.
QTimer::singleShot(UPLOAD_DELAY, [channel, &textEdit]() {
uploadImageToNuuls(uploadQueue.front(), channel, textEdit);
uploadQueue.pop();
});
}
logToFile(originalFilePath, link, deletionLink, channel);
})
.onError([channel](NetworkResult result) -> bool {
auto errorMessage =
QString("An error happened while uploading your image: %1")
.arg(result.formatError());
// Try to read more information from the result body
auto obj = result.parseJson();
if (!obj.isEmpty())
{
auto apiCode = obj.value("code");
if (!apiCode.isUndefined())
{
auto codeString = apiCode.toVariant().toString();
codeString.truncate(20);
errorMessage += QString(" - code: %1").arg(codeString);
}
auto apiError = obj.value("error").toString();
if (!apiError.isEmpty())
{
apiError.truncate(300);
errorMessage +=
QString(" - error: %1").arg(apiError.trimmed());
}
}
channel->addMessage(makeSystemMessage(errorMessage));
uploadMutex.unlock();
return true; return true;
}) })
.execute(); .execute();
} }
void upload(const QMimeData *source, ChannelPtr channel, void ImageUploader::handleFailedUpload(const NetworkResult &result,
ResizingTextEdit &outputTextEdit) ChannelPtr channel)
{ {
if (!uploadMutex.tryLock()) auto errorMessage =
QString("An error happened while uploading your image: %1")
.arg(result.formatError());
// Try to read more information from the result body
auto obj = result.parseJson();
if (!obj.isEmpty())
{
auto apiCode = obj.value("code");
if (!apiCode.isUndefined())
{
auto codeString = apiCode.toVariant().toString();
codeString.truncate(20);
errorMessage += QString(" - code: %1").arg(codeString);
}
auto apiError = obj.value("error").toString();
if (!apiError.isEmpty())
{
apiError.truncate(300);
errorMessage += QString(" - error: %1").arg(apiError.trimmed());
}
}
channel->addMessage(makeSystemMessage(errorMessage));
this->uploadMutex_.unlock();
}
void ImageUploader::handleSuccessfulUpload(const NetworkResult &result,
QString originalFilePath,
ChannelPtr channel,
QPointer<ResizingTextEdit> textEdit)
{
if (textEdit == nullptr)
{
// Split was destroyed abort further uploads
while (!this->uploadQueue_.empty())
{
this->uploadQueue_.pop();
}
this->uploadMutex_.unlock();
return;
}
QString link =
getSettings()->imageUploaderLink.getValue().isEmpty()
? result.getData()
: getLinkFromResponse(result, getSettings()->imageUploaderLink);
QString deletionLink =
getSettings()->imageUploaderDeletionLink.getValue().isEmpty()
? ""
: getLinkFromResponse(result,
getSettings()->imageUploaderDeletionLink);
qCDebug(chatterinoImageuploader) << link << deletionLink;
textEdit->insertPlainText(link + " ");
// 2 seconds for the timer that's there not to spam the remote server
// and 1 second of actual uploading.
auto timeToUpload = this->uploadQueue_.size() * (UPLOAD_DELAY / 1000 + 1);
MessageBuilder builder(imageUploaderResultMessage, link, deletionLink,
this->uploadQueue_.size(), timeToUpload);
channel->addMessage(builder.release());
if (this->uploadQueue_.empty())
{
this->uploadMutex_.unlock();
}
else
{
QTimer::singleShot(UPLOAD_DELAY, [channel, &textEdit, this]() {
this->sendImageUploadRequest(this->uploadQueue_.front(), channel,
textEdit);
this->uploadQueue_.pop();
});
}
this->logToFile(originalFilePath, link, deletionLink, channel);
}
void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
QPointer<ResizingTextEdit> outputTextEdit)
{
if (!this->uploadMutex_.tryLock())
{ {
channel->addMessage(makeSystemMessage( channel->addMessage(makeSystemMessage(
QString("Please wait until the upload finishes."))); QString("Please wait until the upload finishes.")));
@ -265,7 +281,7 @@ void upload(const QMimeData *source, ChannelPtr channel,
{ {
channel->addMessage( channel->addMessage(
makeSystemMessage(QString("Couldn't load image :("))); makeSystemMessage(QString("Couldn't load image :(")));
uploadMutex.unlock(); this->uploadMutex_.unlock();
return; return;
} }
@ -273,7 +289,7 @@ void upload(const QMimeData *source, ChannelPtr channel,
if (imageData) if (imageData)
{ {
RawImageData data = {*imageData, "png", localPath}; RawImageData data = {*imageData, "png", localPath};
uploadQueue.push(data); this->uploadQueue_.push(data);
} }
else else
{ {
@ -281,7 +297,7 @@ void upload(const QMimeData *source, ChannelPtr channel,
QString("Cannot upload file: %1. Couldn't convert " QString("Cannot upload file: %1. Couldn't convert "
"image to png.") "image to png.")
.arg(localPath))); .arg(localPath)));
uploadMutex.unlock(); this->uploadMutex_.unlock();
return; return;
} }
} }
@ -295,11 +311,11 @@ void upload(const QMimeData *source, ChannelPtr channel,
{ {
channel->addMessage( channel->addMessage(
makeSystemMessage(QString("Failed to open file. :("))); makeSystemMessage(QString("Failed to open file. :(")));
uploadMutex.unlock(); this->uploadMutex_.unlock();
return; return;
} }
RawImageData data = {file.readAll(), "gif", localPath}; RawImageData data = {file.readAll(), "gif", localPath};
uploadQueue.push(data); this->uploadQueue_.push(data);
file.close(); file.close();
// file.readAll() => might be a bit big but it /should/ work // file.readAll() => might be a bit big but it /should/ work
} }
@ -308,31 +324,32 @@ void upload(const QMimeData *source, ChannelPtr channel,
channel->addMessage(makeSystemMessage( channel->addMessage(makeSystemMessage(
QString("Cannot upload file: %1. Not an image.") QString("Cannot upload file: %1. Not an image.")
.arg(localPath))); .arg(localPath)));
uploadMutex.unlock(); this->uploadMutex_.unlock();
return; return;
} }
} }
if (!uploadQueue.empty()) if (!this->uploadQueue_.empty())
{ {
uploadImageToNuuls(uploadQueue.front(), channel, outputTextEdit); this->sendImageUploadRequest(this->uploadQueue_.front(), channel,
uploadQueue.pop(); outputTextEdit);
this->uploadQueue_.pop();
} }
} }
else if (source->hasFormat("image/png")) else if (source->hasFormat("image/png"))
{ {
// the path to file is not present every time, thus the filePath is empty // the path to file is not present every time, thus the filePath is empty
uploadImageToNuuls({source->data("image/png"), "png", ""}, channel, this->sendImageUploadRequest({source->data("image/png"), "png", ""},
outputTextEdit); channel, outputTextEdit);
} }
else if (source->hasFormat("image/jpeg")) else if (source->hasFormat("image/jpeg"))
{ {
uploadImageToNuuls({source->data("image/jpeg"), "jpeg", ""}, channel, this->sendImageUploadRequest({source->data("image/jpeg"), "jpeg", ""},
outputTextEdit); channel, outputTextEdit);
} }
else if (source->hasFormat("image/gif")) else if (source->hasFormat("image/gif"))
{ {
uploadImageToNuuls({source->data("image/gif"), "gif", ""}, channel, this->sendImageUploadRequest({source->data("image/gif"), "gif", ""},
outputTextEdit); channel, outputTextEdit);
} }
else else
@ -341,14 +358,14 @@ void upload(const QMimeData *source, ChannelPtr channel,
auto imageData = convertToPng(image); auto imageData = convertToPng(image);
if (imageData) if (imageData)
{ {
uploadImageToNuuls({*imageData, "png", ""}, channel, sendImageUploadRequest({*imageData, "png", ""}, channel,
outputTextEdit); outputTextEdit);
} }
else else
{ {
channel->addMessage(makeSystemMessage( channel->addMessage(makeSystemMessage(
QString("Cannot upload file, failed to convert to png."))); QString("Cannot upload file, failed to convert to png.")));
uploadMutex.unlock(); this->uploadMutex_.unlock();
} }
} }
} }

View file

@ -0,0 +1,49 @@
#pragma once
#include "common/Singleton.hpp"
#include <QMimeData>
#include <QMutex>
#include <QString>
#include <memory>
#include <queue>
namespace chatterino {
class ResizingTextEdit;
class Channel;
class NetworkResult;
using ChannelPtr = std::shared_ptr<Channel>;
struct RawImageData {
QByteArray data;
QString format;
QString filePath;
};
class ImageUploader final : public Singleton
{
public:
void save() override;
void upload(const QMimeData *source, ChannelPtr channel,
QPointer<ResizingTextEdit> outputTextEdit);
private:
void sendImageUploadRequest(RawImageData imageData, ChannelPtr channel,
QPointer<ResizingTextEdit> textEdit);
// This is called from the onSuccess handler of the NetworkRequest in sendImageUploadRequest
void handleSuccessfulUpload(const NetworkResult &result,
QString originalFilePath, ChannelPtr channel,
QPointer<ResizingTextEdit> textEdit);
void handleFailedUpload(const NetworkResult &result, ChannelPtr channel);
void logToFile(const QString &originalFilePath, const QString &imageLink,
const QString &deletionLink, ChannelPtr channel);
// These variables are only used from the main thread.
QMutex uploadMutex_;
std::queue<RawImageData> uploadQueue_;
};
} // namespace chatterino

View file

@ -1,27 +0,0 @@
#pragma once
#include <QMimeData>
#include <QString>
#include <memory>
namespace chatterino {
class ResizingTextEdit;
class Channel;
using ChannelPtr = std::shared_ptr<Channel>;
struct RawImageData {
QByteArray data;
QString format;
QString filePath;
};
void upload(QByteArray imageData, ChannelPtr channel,
ResizingTextEdit &textEdit, std::string format);
void upload(RawImageData imageData, ChannelPtr channel,
ResizingTextEdit &textEdit);
void upload(const QMimeData *source, ChannelPtr channel,
ResizingTextEdit &outputTextEdit);
} // namespace chatterino

View file

@ -16,12 +16,12 @@
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
#include "singletons/Fonts.hpp" #include "singletons/Fonts.hpp"
#include "singletons/ImageUploader.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "util/Clipboard.hpp" #include "util/Clipboard.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#include "util/NuulsUploader.hpp"
#include "util/StreamLink.hpp" #include "util/StreamLink.hpp"
#include "widgets/dialogs/QualityPopup.hpp" #include "widgets/dialogs/QualityPopup.hpp"
#include "widgets/dialogs/SelectChannelDialog.hpp" #include "widgets/dialogs/SelectChannelDialog.hpp"
@ -405,7 +405,8 @@ Split::Split(QWidget *parent)
getSettings()->askOnImageUpload.setValue(false); getSettings()->askOnImageUpload.setValue(false);
} }
} }
upload(source, this->getChannel(), *this->input_->ui_.textEdit); QPointer<ResizingTextEdit> edit = this->input_->ui_.textEdit;
getApp()->imageUploader->upload(source, this->getChannel(), edit);
}); });
getSettings()->imageUploaderEnabled.connect( getSettings()->imageUploaderEnabled.connect(