Add support for more image uploader services (#1741)

The list of links that have been uploaded is now also stored in the json format instead of csv
This commit is contained in:
Paweł
2020-07-05 14:32:10 +02:00
committed by GitHub
parent b66c2478a0
commit 682caf6b69
11 changed files with 255 additions and 86 deletions
-4
View File
@@ -57,10 +57,6 @@ Env::Env()
, twitchEmoteSetResolverUrl(readStringEnv(
"CHATTERINO2_TWITCH_EMOTE_SET_RESOLVER_URL",
"https://braize.pajlada.com/chatterino/twitchemotes/set/%1/"))
, imageUploaderUrl(readStringEnv("CHATTERINO2_IMAGE_UPLOADER_URL",
"https://i.nuuls.com/upload"))
, imageUploaderFormBody(
readStringEnv("CHATTERINO2_IMAGE_UPLOADER_FORM_BODY", "attachment"))
, twitchServerHost(
readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv"))
, twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 443))
-2
View File
@@ -14,8 +14,6 @@ public:
const QString recentMessagesApiUrl;
const QString linkResolverUrl;
const QString twitchEmoteSetResolverUrl;
const QString imageUploaderUrl;
const QString imageUploaderFormBody;
const QString twitchServerHost;
const uint16_t twitchServerPort;
const bool twitchServerSecure;
+14
View File
@@ -99,6 +99,20 @@ NetworkRequest NetworkRequest::header(const char *headerName,
return std::move(*this);
}
NetworkRequest NetworkRequest::headerList(const QStringList &headers) &&
{
for (const QString &header : headers)
{
const QStringList thisHeader = header.trimmed().split(":");
if (thisHeader.size() == 2)
{
this->data->request_.setRawHeader(thisHeader[0].trimmed().toUtf8(),
thisHeader[1].trimmed().toUtf8());
}
}
return std::move(*this);
}
NetworkRequest NetworkRequest::timeout(int ms) &&
{
this->data->hasTimeout_ = true;
+2
View File
@@ -3,6 +3,7 @@
#include "common/NetworkCommon.hpp"
#include "common/NetworkResult.hpp"
#include <QHttpMultiPart>
#include <memory>
namespace chatterino {
@@ -52,6 +53,7 @@ public:
NetworkRequest header(const char *headerName, const char *value) &&;
NetworkRequest header(const char *headerName, const QByteArray &value) &&;
NetworkRequest header(const char *headerName, const QString &value) &&;
NetworkRequest headerList(const QStringList &headers) &&;
NetworkRequest timeout(int ms) &&;
NetworkRequest concurrent() &&;
NetworkRequest authorizeTwitchV5(const QString &clientID,
+11
View File
@@ -273,6 +273,17 @@ public:
// Custom URI Scheme
QStringSetting customURIScheme = {"/external/urischeme"};
// Image Uploader
QStringSetting imageUploaderUrl = {"/external/imageUploader/url",
"https://i.nuuls.com/upload"};
QStringSetting imageUploaderFormField = {
"/external/imageUploader/formField", "attachment"};
QStringSetting imageUploaderHeaders = {"/external/imageUploader/headers",
""};
QStringSetting imageUploaderLink = {"/external/imageUploader/link", ""};
QStringSetting imageUploaderDeletionLink = {
"/external/imageUploader/deletionLink", ""};
/// Misc
BoolSetting betaUpdates = {"/misc/beta", false};
#ifdef Q_OS_LINUX
+104 -37
View File
@@ -5,11 +5,15 @@
#include "providers/twitch/TwitchMessageBuilder.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
#include "util/CombinePath.hpp"
#include <QBuffer>
#include <QHttpMultiPart>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeDatabase>
#include <QMutex>
#include <QSaveFile>
#define UPLOAD_DELAY 2000
// Delay between uploads in milliseconds
@@ -38,37 +42,76 @@ namespace chatterino {
static auto uploadMutex = QMutex();
static std::queue<RawImageData> uploadQueue;
//logging information on successful uploads to a csv file
void logToCsv(const QString originalFilePath, const QString link,
ChannelPtr channel)
// logging information on successful uploads to a json file
void logToFile(const QString originalFilePath, QString imageLink,
QString deletionLink, ChannelPtr channel)
{
const QString csvFileName = (getSettings()->logPath.getValue().isEmpty()
? getPaths()->messageLogDirectory
: getSettings()->logPath) +
"/ImageUploader.csv";
QFile csvFile(csvFileName);
bool csvExisted = csvFile.exists();
bool isCsvOkay = csvFile.open(QIODevice::Append | QIODevice::Text);
if (!isCsvOkay)
const QString logFileName =
combinePath((getSettings()->logPath.getValue().isEmpty()
? getPaths()->messageLogDirectory
: getSettings()->logPath),
"ImageUploader.json");
//reading existing logs
QFile logReadFile(logFileName);
bool isLogFileOkay =
logReadFile.open(QIODevice::ReadWrite | QIODevice::Text);
if (!isLogFileOkay)
{
channel->addMessage(makeSystemMessage(
QString("Failed to open csv file with links at ") + csvFileName));
QString("Failed to open log file with links at ") + logFileName));
return;
}
QTextStream out(&csvFile);
qDebug() << csvExisted;
if (!csvExisted)
auto logs = logReadFile.readAll();
if (logs.isEmpty())
{
out << "localPath,imageLink,timestamp,channelName\n";
logs = QJsonDocument(QJsonArray()).toJson();
}
out << originalFilePath + QString(",") << link + QString(",")
<< QDateTime::currentSecsSinceEpoch()
<< QString(",%1\n").arg(channel->getName());
// image path (can be empty)
// image link
// timestamp
logReadFile.close();
//writing new data to logs
QJsonObject newLogEntry;
newLogEntry["channelName"] = channel->getName();
newLogEntry["deletionLink"] =
deletionLink.isEmpty() ? QJsonValue(QJsonValue::Null) : deletionLink;
newLogEntry["imageLink"] = imageLink;
newLogEntry["localPath"] = originalFilePath.isEmpty()
? QJsonValue(QJsonValue::Null)
: originalFilePath;
newLogEntry["timestamp"] = QDateTime::currentSecsSinceEpoch();
// channel name
csvFile.close();
// deletion link (can be empty)
// image link
// local path to an image (can be empty)
// timestamp
QSaveFile logSaveFile(logFileName);
logSaveFile.open(QIODevice::WriteOnly | QIODevice::Text);
QJsonArray entries = QJsonDocument::fromJson(logs).array();
entries.push_back(newLogEntry);
logSaveFile.write(QJsonDocument(entries).toJson());
logSaveFile.commit();
}
// extracting link to either image or its deletion from response body
QString getJSONValue(QJsonValue responseJson, QString jsonPattern)
{
for (const QString &key : jsonPattern.split("."))
{
responseJson = responseJson[key];
}
return responseJson.toString();
}
QString getLinkFromResponse(NetworkResult response, QString pattern)
{
QRegExp regExp("\\{(.+)\\}");
regExp.setMinimal(true);
while (regExp.indexIn(pattern) != -1)
{
pattern.replace(regExp.cap(0),
getJSONValue(response.parseJson(), regExp.cap(1)));
}
return pattern;
}
void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel,
@@ -77,8 +120,15 @@ void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel,
const static char *const boundary = "thisistheboudaryasd";
const static QString contentType =
QString("multipart/form-data; boundary=%1").arg(boundary);
static QUrl url(Env::get().imageUploaderUrl);
static QString formBody(Env::get().imageUploaderFormBody);
QUrl url(getSettings()->imageUploaderUrl.getValue().isEmpty()
? getSettings()->imageUploaderUrl.getDefaultValue()
: getSettings()->imageUploaderUrl);
QString formField(
getSettings()->imageUploaderFormField.getValue().isEmpty()
? getSettings()->imageUploaderFormField.getDefaultValue()
: getSettings()->imageUploaderFormField);
QStringList extraHeaders(
getSettings()->imageUploaderHeaders.getValue().split(";"));
QString originalFilePath = imageData.filePath;
QHttpMultiPart *payload = new QHttpMultiPart(QHttpMultiPart::FormDataType);
@@ -90,32 +140,50 @@ void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel,
QVariant(imageData.data.length()));
part.setHeader(QNetworkRequest::ContentDispositionHeader,
QString("form-data; name=\"%1\"; filename=\"control_v.%2\"")
.arg(formBody)
.arg(formField)
.arg(imageData.format));
payload->setBoundary(boundary);
payload->append(part);
NetworkRequest(url, NetworkRequestType::Post)
.header("Content-Type", contentType)
.headerList(extraHeaders)
.multiPart(payload)
.onSuccess([&textEdit, channel,
originalFilePath](NetworkResult result) -> Outcome {
textEdit.insertPlainText(result.getData() + QString(" "));
QString link = getSettings()->imageUploaderLink.getValue().isEmpty()
? result.getData()
: getLinkFromResponse(
result, getSettings()->imageUploaderLink);
QString deletionLink =
getSettings()->imageUploaderDeletionLink.getValue().isEmpty()
? ""
: getLinkFromResponse(
result, getSettings()->imageUploaderDeletionLink);
qDebug() << link << deletionLink;
textEdit.insertPlainText(link + " ");
if (uploadQueue.empty())
{
channel->addMessage(makeSystemMessage(
QString("Your image has been uploaded to ") +
result.getData()));
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 left. Please "
"wait until all of them are uploaded. About %3 "
"seconds left.")
.arg(result.getData() + QString(""))
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
@@ -127,7 +195,7 @@ void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel,
});
}
logToCsv(originalFilePath, result.getData(), channel);
logToFile(originalFilePath, link, deletionLink, channel);
return Success;
})
@@ -161,7 +229,6 @@ void upload(const QMimeData *source, ChannelPtr channel,
{
QString localPath = path.toLocalFile();
QMimeType mime = mimeDb.mimeTypeForUrl(path);
qDebug() << mime.name();
if (mime.name().startsWith("image") && !mime.inherits("image/gif"))
{
channel->addMessage(makeSystemMessage(
@@ -3,8 +3,11 @@
#include "Application.hpp"
#include "util/Helpers.hpp"
#include "util/LayoutCreator.hpp"
#include "util/RemoveScrollAreaBackground.hpp"
#include <QFormLayout>
#include <QGroupBox>
#include <QLabel>
#define STREAMLINK_QUALITY \
"Choose", "Source", "High", "Medium", "Low", "Audio only"
@@ -14,7 +17,12 @@ namespace chatterino {
ExternalToolsPage::ExternalToolsPage()
{
LayoutCreator<ExternalToolsPage> layoutCreator(this);
auto layout = layoutCreator.setLayoutType<QVBoxLayout>();
auto scroll = layoutCreator.emplace<QScrollArea>();
auto widget = scroll.emplaceScrollAreaWidget();
removeScrollAreaBackground(scroll.getElement(), widget.getElement());
auto layout = widget.setLayoutType<QVBoxLayout>();
{
auto group = layout.emplace<QGroupBox>("Streamlink");
@@ -37,7 +45,7 @@ ExternalToolsPage::ExternalToolsPage()
links->setTextFormat(Qt::RichText);
links->setTextInteractionFlags(Qt::TextBrowserInteraction |
Qt::LinksAccessibleByKeyboard |
Qt::LinksAccessibleByKeyboard);
Qt::LinksAccessibleByMouse);
links->setOpenExternalLinks(true);
groupLayout->setWidget(0, QFormLayout::SpanningRole, description);
@@ -85,6 +93,44 @@ ExternalToolsPage::ExternalToolsPage()
getSettings()->customURIScheme));
}
{
auto group = layout.emplace<QGroupBox>("Image Uploader");
auto groupLayout = group.setLayoutType<QFormLayout>();
const auto description = new QLabel(
"You can set custom host for uploading images, like "
"imgur.com or s-ul.eu.<br>Check " +
formatRichNamedLink("https://github.com/Chatterino/chatterino2/"
"blob/master/docs/IMAGEUPLOADER.md",
"this guide") +
" for help.");
description->setWordWrap(true);
description->setStyleSheet("color: #bbb");
description->setTextFormat(Qt::RichText);
description->setTextInteractionFlags(Qt::TextBrowserInteraction |
Qt::LinksAccessibleByKeyboard |
Qt::LinksAccessibleByMouse);
description->setOpenExternalLinks(true);
groupLayout->setWidget(0, QFormLayout::SpanningRole, description);
groupLayout->addRow(
"Request URL: ",
this->createLineEdit(getSettings()->imageUploaderUrl));
groupLayout->addRow(
"Form field: ",
this->createLineEdit(getSettings()->imageUploaderFormField));
groupLayout->addRow(
"Extra Headers: ",
this->createLineEdit(getSettings()->imageUploaderHeaders));
groupLayout->addRow(
"Image link: ",
this->createLineEdit(getSettings()->imageUploaderLink));
groupLayout->addRow(
"Deletion link: ",
this->createLineEdit(getSettings()->imageUploaderDeletionLink));
}
layout->addStretch(1);
}
+26 -25
View File
@@ -207,33 +207,34 @@ Split::Split(QWidget *parent)
[this] { this->focused.invoke(); });
this->input_->ui_.textEdit->focusLost.connect(
[this] { this->focusLost.invoke(); });
this->input_->ui_.textEdit->imagePasted.connect([this](const QMimeData
*source) {
if (getSettings()->askOnImageUpload.getValue())
{
QMessageBox msgBox;
msgBox.setText("Image upload");
msgBox.setInformativeText(
"You are uploading an image to i.nuuls.com. You won't be able "
"to remove the image from the site. Are you okay with this?");
msgBox.addButton(QMessageBox::Cancel);
msgBox.addButton(QMessageBox::Yes);
msgBox.addButton("Yes, don't ask again", QMessageBox::YesRole);
msgBox.setDefaultButton(QMessageBox::Yes);
auto picked = msgBox.exec();
if (picked == QMessageBox::Cancel)
this->input_->ui_.textEdit->imagePasted.connect(
[this](const QMimeData *source) {
if (getSettings()->askOnImageUpload.getValue())
{
return;
QMessageBox msgBox;
msgBox.setText("Image upload");
msgBox.setInformativeText(
"You are uploading an image to an external server. You may "
"not be able to remove the image from the site. Are you "
"okay with this?");
msgBox.addButton(QMessageBox::Cancel);
msgBox.addButton(QMessageBox::Yes);
msgBox.addButton("Yes, don't ask again", QMessageBox::YesRole);
msgBox.setDefaultButton(QMessageBox::Yes);
auto picked = msgBox.exec();
if (picked == QMessageBox::Cancel)
{
return;
}
else if (picked == 0) // don't ask again button
{
getSettings()->askOnImageUpload.setValue(false);
}
}
else if (picked == 0) // don't ask again button
{
getSettings()->askOnImageUpload.setValue(false);
}
}
upload(source, this->getChannel(), *this->input_->ui_.textEdit);
});
upload(source, this->getChannel(), *this->input_->ui_.textEdit);
});
setAcceptDrops(true);
}