...
 
Commits (4)
......@@ -38,6 +38,7 @@ SOURCES += \
src/settings/accountswidget.cpp \
src/uploaddialog.cpp \
src/uploadqueuewidget.cpp \
src/uploadqueueworker.cpp \
src/webdialog.cpp \
src/models/appsetting.cpp \
src/models/fauser.cpp \
......@@ -55,6 +56,7 @@ HEADERS += \
src/settings/accountswidget.h \
src/uploaddialog.h \
src/uploadqueuewidget.h \
src/uploadqueueworker.h \
src/webdialog.h \
src/models/appsetting.h \
src/models/fauser.h \
......
This diff is collapsed.
......@@ -14,6 +14,9 @@
//! The base URL for accessing FurAffinity.
#define FURAFFINITY_BASE_URL "https://www.furaffinity.net"
#define FURAFFINITY_BASE_URL "http://127.0.0.1:9292"
#define CLIENT_USER_AGENT "Mozilla/5.0 (compatible; nFurAffinity) https://github.com/nilsding/nFurAffinity"
//!
//! \brief The FurAffinityClient class makes HTTP requests against FurAffinity
......@@ -36,6 +39,10 @@ public:
void updateMsgOthers();
void initSubmission(qlonglong uploadId);
void uploadSubmissionFile(qlonglong uploadId);
void finaliseSubmission(qlonglong uploadId);
QString cookieA() const
{
return m_cookieA;
......@@ -66,6 +73,7 @@ public:
return m_userId != -1;
}
signals:
void cookieAChanged(QString cookieA);
......@@ -75,6 +83,8 @@ signals:
void userIdChanged(int userId);
void uploadUpdated(qlonglong upload, int newPart);
public slots:
private:
......@@ -87,15 +97,18 @@ private:
enum RequestOrigin {
Unknown = 0, //!< unknown request origin, this usually means invalid
MsgOthers = 1
MsgOthers = 1, //!< GET /msg/others/
SubmitPart2, //!< POST /submit/ {part: 2, submission_type: "submission"}
SubmitPart3, //!< POST /submit/ {part: 3, with file upload}
SubmitPart5 //!< POST /submit/ {part: 5, submission info}
};
//! RequestOrigin -> function mapping
static QHash<RequestOrigin,
std::function<void(FurAffinityClient*, KruzifiXMLParser)> >
std::function<void(FurAffinityClient*, KruzifiXMLParser, QNetworkReply*)> >
RequestOriginFunctionHash;
void handleMsgOthers(KruzifiXMLParser parser);
void handleMsgOthers(KruzifiXMLParser parser, QNetworkReply* reply);
void parseAndStoreFavorites(KruzifiXMLParser parser);
void parseFavorites_Persist(const QString &username,
qlonglong submissionId,
......@@ -103,6 +116,12 @@ private:
qlonglong favId,
const QDateTime &favDate);
void handleSubmitPart2(KruzifiXMLParser parser, QNetworkReply* reply);
void handleSubmitPart3(KruzifiXMLParser parser, QNetworkReply* reply);
void handleSubmitPart5(KruzifiXMLParser parser, QNetworkReply* reply);
void updateUploadKey(int targetPart, KruzifiXMLParser parser, QNetworkReply* reply);
RequestOrigin getRequestOriginFromNetworkReply(QNetworkReply *reply);
private slots:
......
......@@ -23,6 +23,8 @@ extern "C" {
const char* kruzifixml_parser_msg_others_favorites_get_submission_title(KruzifiXMLFavInfo favinfo);
long kruzifixml_parser_msg_others_favorites_get_fav_date(KruzifiXMLFavInfo favinfo);
void kruzifixml_parser_msg_others_favorites_delete(KruzifiXMLFavInfoList list);
const char* kruzifixml_parser_submit_key(KruzifiXMLParser parser);
}
#endif // KRUZIFIXML_H
This diff is collapsed.
......@@ -2,6 +2,7 @@ require "./spec_helper"
describe KruzifiXML::Parser do
context "/msg/others response" do
next # don't have the fixture anymore
html = File.read(File.expand_path("./fixtures/msg_others.html", __DIR__))
it "parses the HTML" do
......@@ -13,4 +14,16 @@ describe KruzifiXML::Parser do
KruzifiXML::Parser.new(html).msg_others_favorites.first.should eq fav_info
end
end
context "/submit/ key extraction" do
html = File.read(File.expand_path("./fixtures/submit_part2.html", __DIR__))
it "parses the HTML" do
KruzifiXML::Parser.new(html)
end
it "extracts the key" do
KruzifiXML::Parser.new(html).submit_key.should eq "77e4e2a0188a57744c62ff9be09ec280ee477f26"
end
end
end
......@@ -21,6 +21,7 @@ end
module KruzifiXML
@@parser_instances = [] of KruzifiXML::Parser
@@fav_info_arrays = [] of Array(KruzifiXML::FavInfo)
@@strings = [] of Array(String)
def self.parser_instances
@@parser_instances
......@@ -92,3 +93,14 @@ fun parser_msg_others_favorites_delete = kruzifixml_parser_msg_others_favorites_
real_instance = Box(KruzifiXML::FavInfo).unbox(instance)
KruzifiXML.parser_instances.delete(real_instance)
end
fun parser_submit_key = kruzifixml_parser_submit_key(instance : Void*)
begin
real_instance = Box(KruzifiXML::Parser).unbox(instance)
key = real_instance.submit_key
key.to_unsafe
rescue e
STDERR.puts "kruzifix nuamoi #{e}"
Pointer(Void).null
end
end
require "xml"
require "./types"
require "./parsers/*"
module KruzifiXML
# Parser for FurAffinity pages
......@@ -8,74 +8,5 @@ module KruzifiXML
def initialize(document : String)
@document = XML.parse_html(document)
end
def msg_others_favorites : Array(FavInfo)
# get the root ul
nodes = @document.xpath(%{(//*[@id="favorites"])})
return [] of FavInfo unless nodes.is_a?(XML::NodeSet)
return [] of FavInfo if nodes.size.zero?
# we got the root ul, but we're interested in its subelements
nodes = nodes.first.children
# format of fav:
# (1) li
# (2) +-- input[type=checkbox, value=:favId]
# (3) +-- a[href=:userUrl] :userName
# (4) +-- text `has favorited "`
# (5) +-- a[href=:submissionId] :submissionTitle
# (6) +-- text `" `
# (7) +-- span[title=:favDate]
nodes.map do |node|
# (1) not a li element? not interested
next unless node.name == "li"
# (2) find fav_id
checkbox_node = node.children.find { |node| node.name == "input" && node["name"]? == "favorites[]" }
if checkbox_node.nil?
STDERR.puts("#{self.class.name}#msg_others_favorites: (2) did not find checkbox")
next
end
fav_id = checkbox_node["value"].to_i64
# (3) get username from fav
username_node = node.children.find { |node| node.name == "a" && node["href"]? =~ %r{^/user/} }
if username_node.nil?
STDERR.puts("#{self.class.name}#msg_others_favorites: (3) did not find username node")
next FavInfo.new(valid: false, fav_id: fav_id)
end
username = username_node.content.strip
# (5) find submission id and title
submission_node = node.children.find { |node| node.name == "a" && node["href"]? =~ %r{^/view/} }
if submission_node.nil?
STDERR.puts("#{self.class.name}#msg_others_favorites: (5) did not find submission node")
next FavInfo.new(valid: false, fav_id: fav_id, username: username)
end
submission_info = {
id: submission_node["href"].gsub(/\D+/, "").to_i64,
title: submission_node.content.strip,
}
# (7) parse the date
date_node = node.children.find { |node| node.name == "span" && node["title"]? }
if date_node.nil?
STDERR.puts("#{self.class.name}#msg_others_favorites: (7) did not find date node")
next FavInfo.new(valid: false, fav_id: fav_id, username: username, submission_id: submission_info[:id], submission_title: submission_info[:title])
end
# parse the date without `1st / 2nd / ...` suffix into local time; FA uses EST
fav_date = Time.parse(date_node["title"].sub(/st|nd|rd|th/, ""), "%b %-d, %Y %I:%M %P", Time::Location.load("EST")).to_unix
FavInfo.new(
valid: true,
fav_id: fav_id,
username: username,
submission_id: submission_info[:id],
submission_title: submission_info[:title],
fav_date: fav_date
)
end.compact
end
end
end
require "../types"
module KruzifiXML
class Parser
def msg_others_favorites : Array(FavInfo)
# get the root ul
nodes = @document.xpath(%{(//*[@id="favorites"])})
return [] of FavInfo unless nodes.is_a?(XML::NodeSet)
return [] of FavInfo if nodes.size.zero?
# we got the root ul, but we're interested in its subelements
nodes = nodes.first.children
# format of fav:
# (1) li
# (2) +-- input[type=checkbox, value=:favId]
# (3) +-- a[href=:userUrl] :userName
# (4) +-- text `has favorited "`
# (5) +-- a[href=:submissionId] :submissionTitle
# (6) +-- text `" `
# (7) +-- span[title=:favDate]
nodes.map do |node|
# (1) not a li element? not interested
next unless node.name == "li"
# (2) find fav_id
checkbox_node = node.children.find { |node| node.name == "input" && node["name"]? == "favorites[]" }
if checkbox_node.nil?
STDERR.puts("#{self.class.name}#msg_others_favorites: (2) did not find checkbox")
next
end
fav_id = checkbox_node["value"].to_i64
# (3) get username from fav
username_node = node.children.find { |node| node.name == "a" && node["href"]? =~ %r{^/user/} }
if username_node.nil?
STDERR.puts("#{self.class.name}#msg_others_favorites: (3) did not find username node")
next FavInfo.new(valid: false, fav_id: fav_id)
end
username = username_node.content.strip
# (5) find submission id and title
submission_node = node.children.find { |node| node.name == "a" && node["href"]? =~ %r{^/view/} }
if submission_node.nil?
STDERR.puts("#{self.class.name}#msg_others_favorites: (5) did not find submission node")
next FavInfo.new(valid: false, fav_id: fav_id, username: username)
end
submission_info = {
id: submission_node["href"].gsub(/\D+/, "").to_i64,
title: submission_node.content.strip,
}
# (7) parse the date
date_node = node.children.find { |node| node.name == "span" && node["title"]? }
if date_node.nil?
STDERR.puts("#{self.class.name}#msg_others_favorites: (7) did not find date node")
next FavInfo.new(valid: false, fav_id: fav_id, username: username, submission_id: submission_info[:id], submission_title: submission_info[:title])
end
# parse the date without `1st / 2nd / ...` suffix into local time; FA uses EST
fav_date = Time.parse(date_node["title"].sub(/st|nd|rd|th/, ""), "%b %-d, %Y %I:%M %P", Time::Location.load("EST")).to_unix
FavInfo.new(
valid: true,
fav_id: fav_id,
username: username,
submission_id: submission_info[:id],
submission_title: submission_info[:title],
fav_date: fav_date
)
end.compact
end
end
end
module KruzifiXML
class Parser
@key = ""
def submit_key : String
# get the root form
nodes = @document.xpath(%{//form[@method="post"]})
return "" unless nodes.is_a?(XML::NodeSet)
return "" if nodes.size.zero?
# check its subelements for the hidden `key` input
key_node = nodes.first.children.find { |node| node.name == "input" && node["name"] == "key" }
if key_node.nil?
STDERR.puts("#{self.class.name}#submit_key: did not find key attribute")
return ""
end
# extract the key from the attribute and return it
@key = key_node["value"]
end
end
end
......@@ -17,7 +17,7 @@ MainWindow::MainWindow(QWidget *parent) :
qagAccounts = new QActionGroup(this);
ui->setupUi(this);
widgetUploadQueue = new UploadQueueWidget(this);
widgetUploadQueue = new UploadQueueWidget(this, client);
widgetUploadQueue->layout()->setMargin(10);
ui->centralTabs->addTab(widgetUploadQueue, widgetUploadQueue->windowTitle());
......
#include "uploadqueuewidget.h"
#include "ui_uploadqueuewidget.h"
#include "uploaddialog.h"
#include "models/upload.h"
#include <QAbstractItemModel>
#include <QSqlRecord>
#include <QSqlField>
#include <QSqlQuery>
#include <QSqlResult>
#include <QMessageBox>
UploadQueueWidget::UploadQueueWidget(QWidget *parent) :
#include "models/upload.h"
#include "uploaddialog.h"
UploadQueueWidget::UploadQueueWidget(QWidget *parent, FurAffinityClient* client) :
QWidget(parent),
ui(new Ui::UploadQueueWidget)
ui(new Ui::UploadQueueWidget),
furAffinityClient(client)
{
ui->setupUi(this);
......@@ -26,11 +26,20 @@ UploadQueueWidget::UploadQueueWidget(QWidget *parent) :
tableModel->select();
ui->qlvUploads->setEditTriggers(QAbstractItemView::NoEditTriggers);
ui->qlvUploads->setModel(tableModel);
// 5 == title
ui->qlvUploads->setModelColumn(5);
queueWorker = new UploadQueueWorker(nullptr, furAffinityClient);
connect(queueWorker, &UploadQueueWorker::statusTextChanged,
this->ui->qlStatus, &QLabel::setText);
connect(queueWorker, &UploadQueueWorker::workingChanged,
this, &UploadQueueWidget::disableUi);
}
UploadQueueWidget::~UploadQueueWidget()
{
queueWorker->deleteLater();
delete ui;
}
......@@ -217,3 +226,29 @@ void UploadQueueWidget::on_qpbMoveDown_clicked()
ui->qlvUploads->selectionModel()
->setCurrentIndex(newIndex, QItemSelectionModel::Select);
}
void UploadQueueWidget::on_qcbUpload_stateChanged(int state)
{
if (state == Qt::CheckState::Checked)
{
queueWorker->setWorking(true);
}
else
{
queueWorker->setWorking(false);
}
}
void UploadQueueWidget::disableUi(bool shouldDisable)
{
ui->qpbAdd->setDisabled(shouldDisable);
ui->qpbEdit->setDisabled(shouldDisable);
ui->qpbDelete->setDisabled(shouldDisable);
ui->qlvUploads->setDisabled(shouldDisable);
ui->qcbUpload->setChecked(shouldDisable);
if (shouldDisable)
{
ui->qpbMoveUp->setDisabled(true);
ui->qpbMoveDown->setDisabled(true);
}
}
......@@ -3,6 +3,10 @@
#include <QWidget>
#include <QSqlTableModel>
#include <QThread>
#include "furaffinityclient.h"
#include "uploadqueueworker.h"
namespace Ui {
class UploadQueueWidget;
......@@ -13,7 +17,7 @@ class UploadQueueWidget : public QWidget
Q_OBJECT
public:
explicit UploadQueueWidget(QWidget *parent = nullptr);
explicit UploadQueueWidget(QWidget *parent = nullptr, FurAffinityClient* client = nullptr);
~UploadQueueWidget();
private slots:
......@@ -29,9 +33,17 @@ private slots:
void on_qpbMoveDown_clicked();
void on_qcbUpload_stateChanged(int state);
void disableUi(bool shouldDisable);
private:
Ui::UploadQueueWidget *ui;
QSqlTableModel *tableModel;
FurAffinityClient* furAffinityClient;
UploadQueueWorker* queueWorker;
};
#endif // UPLOADQUEUEWIDGET_H
......@@ -141,24 +141,11 @@
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="2,1">
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="2">
<item>
<widget class="QLabel" name="qlStatus">
<property name="text">
<string>Done</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="qpbCurrent">
<property name="maximum">
<number>100</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
<property name="textDirection">
<enum>QProgressBar::TopToBottom</enum>
<string>Ready</string>
</property>
</widget>
</item>
......
#include "uploadqueueworker.h"
#include <QTimer>
#include <QSqlQuery>
#include <QDebug>
#include "models/upload.h"
UploadQueueWorker::UploadQueueWorker(QObject *parent, FurAffinityClient* client) :
QObject(parent),
furAffinityClient(client)
{
connect(this, &UploadQueueWorker::workingChanged,
this, &UploadQueueWorker::onWorkingChanged);
connect(this, &UploadQueueWorker::processingChanged,
this, &UploadQueueWorker::onProcessingChanged);
connect(furAffinityClient, &FurAffinityClient::uploadUpdated,
this, &UploadQueueWorker::onUploadUpdated);
}
void UploadQueueWorker::perform()
{
if (!m_working)
{
// we should no longer process further uploads --> no more processing required
setProcessing(false);
return;
}
QSqlQuery q;
q.prepare("SELECT id FROM `uploads` WHERE `completed_at` IS NULL ORDER BY `sort_id` ASC LIMIT 1");
q.exec();
if (!q.first())
{
// we are done!
setProcessing(false);
setWorking(false);
return;
}
auto upload = Upload::find(q.value("id").toLongLong());
emit(statusTextChanged(tr("Initialising upload “%1”").arg(upload->title())));
furAffinityClient->initSubmission(upload->id());
upload->deleteLater();
}
void UploadQueueWorker::onUploadUpdated(qlonglong uploadId, int newPart)
{
auto upload = Upload::find(uploadId);
switch (newPart)
{
case 3:
{
emit(statusTextChanged(tr("Uploading image “%1”").arg(upload->title())));
furAffinityClient->uploadSubmissionFile(uploadId);
break;
}
case 4:
{
emit(statusTextChanged(tr("Finalising “%1”").arg(upload->title())));
furAffinityClient->finaliseSubmission(uploadId);
break;
}
case 5:
{
setProcessing(false);
startBackoff(30);
break;
}
}
upload->deleteLater();
}
void UploadQueueWorker::onWorkingChanged(bool working)
{
if (working && !m_processing)
{
startBackoff();
}
else if (!working && !m_processing)
{
emit(statusTextChanged(tr("Ready")));
}
}
void UploadQueueWorker::onProcessingChanged(bool processing)
{
if (m_working && processing)
{
QTimer::singleShot(0, this, &UploadQueueWorker::perform);
}
}
void UploadQueueWorker::countdownBackoff()
{
if (m_processing)
{
return;
}
if (!m_working)
{
emit(statusTextChanged(tr("Ready")));
return;
}
m_countdown -= 1;
if (m_countdown > 0)
{
emit(statusTextChanged(tr("Waiting %1s...").arg(m_countdown)));
QTimer::singleShot(1000, this, &UploadQueueWorker::countdownBackoff);
return;
}
setProcessing(true);
}
void UploadQueueWorker::startBackoff(int waitTime)
{
if (m_working && m_processing)
{
// nothing to do here, continue as usual.
return;
}
m_countdown = waitTime;
emit(statusTextChanged(tr("Waiting %1s...").arg(m_countdown)));
QTimer::singleShot(1000, this, &UploadQueueWorker::countdownBackoff);
}
#ifndef UPLOADQUEUEWORKER_H
#define UPLOADQUEUEWORKER_H
#include <QObject>
#include "furaffinityclient.h"
class UploadQueueWorker : public QObject
{
Q_OBJECT
Q_PROPERTY(bool working READ working WRITE setWorking NOTIFY workingChanged)
Q_PROPERTY(bool processing READ processing NOTIFY processingChanged)
public:
explicit UploadQueueWorker(QObject *parent = nullptr, FurAffinityClient* client = nullptr);
//!
//! \brief working returns whether the worker can do work.
//! \return
//!
bool working() const
{
return m_working;
}
//!
//! \brief processing returns whether the worker is currently processing
//! a record.
//! \return
//!
bool processing() const
{
return m_processing;
}
signals:
void workingChanged(bool working);
void processingChanged(bool processing);
void statusTextChanged(const QString &updatedStatusText);
public slots:
void setWorking(bool working)
{
if (m_working == working)
return;
m_working = working;
emit workingChanged(m_working);
}
private slots:
void perform();
void onUploadUpdated(qlonglong uploadId, int newPart);
void onWorkingChanged(bool working);
void onProcessingChanged(bool processing);
void countdownBackoff();
void setProcessing(bool processing)
{
if (m_processing == processing)
return;
m_processing = processing;
emit processingChanged(m_processing);
}
private:
FurAffinityClient* furAffinityClient;
bool m_working = false;
bool m_processing = false;
int m_countdown = 3;
void startBackoff(int waitTime = 3);
};
#endif // UPLOADQUEUEWORKER_H
......@@ -30,7 +30,7 @@ post "/submit/?" do
session[:submission][:key] = SecureRandom.hex(20)
session[:submission][:filename] = params["submission"]["filename"]
redirect "/submit/submission/4/", 301
redirect "/submit/submission/4/", 302
when 5
raise ArgumentError, "key does not match" unless session[:submission][:key] == params["key"]
......