Support for SimplyPrint cloud integration (#4525)

* Make httpserver more generic and reusable

* Add OAuthJob

* Fix issue caused by the fact that the backing widget of the `TextCtrl` is no longer `wxTextCtrl`

* Implement login and token refresh

* Implement file upload

* Try fix build error

* Support BBL printers

* Show error message if user hasn't done OAuth

* Fix typo

* Update error message

* Disable unsupported options when SimplyPrint is selected
This commit is contained in:
Noisyfox 2024-03-23 10:08:48 +08:00 committed by GitHub
parent f3b3e92782
commit e29bbac193
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1075 additions and 228 deletions

View file

@ -874,7 +874,7 @@ static std::vector<std::string> s_Preset_printer_options {
"nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure", "nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure",
"best_object_pos","head_wrap_detect_zone", "best_object_pos","head_wrap_detect_zone",
//SoftFever //SoftFever
"host_type", "print_host", "printhost_apikey", "host_type", "print_host", "printhost_apikey", "bbl_use_printhost",
"print_host_webui", "print_host_webui",
"printhost_cafile","printhost_port","printhost_authorization_type", "printhost_cafile","printhost_port","printhost_authorization_type",
"printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format", "printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format",
@ -2941,6 +2941,7 @@ static std::vector<std::string> s_PhysicalPrinter_opts {
"preset_name", // temporary option to compatibility with older Slicer "preset_name", // temporary option to compatibility with older Slicer
"preset_names", "preset_names",
"printer_technology", "printer_technology",
"bbl_use_printhost",
"host_type", "host_type",
"print_host", "print_host",
"print_host_webui", "print_host_webui",

View file

@ -344,6 +344,13 @@ VendorType PresetBundle::get_current_vendor_type()
return t; return t;
} }
bool PresetBundle::use_bbl_network()
{
const auto cfg = printers.get_edited_preset().config;
const bool use_bbl_network = is_bbl_vendor() && !cfg.opt_bool("bbl_use_printhost");
return use_bbl_network;
}
//BBS: load project embedded presets //BBS: load project embedded presets
PresetsConfigSubstitutions PresetBundle::load_project_embedded_presets(std::vector<Preset*> project_presets, ForwardCompatibilitySubstitutionRule substitution_rule) PresetsConfigSubstitutions PresetBundle::load_project_embedded_presets(std::vector<Preset*> project_presets, ForwardCompatibilitySubstitutionRule substitution_rule)
{ {

View file

@ -97,6 +97,7 @@ public:
VendorType get_current_vendor_type(); VendorType get_current_vendor_type();
// Vendor related handy functions // Vendor related handy functions
bool is_bbl_vendor() { return get_current_vendor_type() == VendorType::Marlin_BBL; } bool is_bbl_vendor() { return get_current_vendor_type() == VendorType::Marlin_BBL; }
bool use_bbl_network();
//BBS: project embedded preset logic //BBS: project embedded preset logic
PresetsConfigSubstitutions load_project_embedded_presets(std::vector<Preset*> project_presets, ForwardCompatibilitySubstitutionRule substitution_rule); PresetsConfigSubstitutions load_project_embedded_presets(std::vector<Preset*> project_presets, ForwardCompatibilitySubstitutionRule substitution_rule);

View file

@ -104,7 +104,8 @@ static t_config_enum_values s_keys_map_PrintHostType {
{ "repetier", htRepetier }, { "repetier", htRepetier },
{ "mks", htMKS }, { "mks", htMKS },
{ "obico", htObico }, { "obico", htObico },
{ "flashforge", htFlashforge} { "flashforge", htFlashforge },
{ "simplyprint", htSimplyPrint },
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType)
@ -542,6 +543,13 @@ void PrintConfigDef::init_common_params()
def->mode = comDevelop; def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings()); def->set_default_value(new ConfigOptionStrings());
def = this->add("bbl_use_printhost", coBool);
def->label = L("Use 3rd-party print host");
def->tooltip = L("Allow controlling BambuLab's printer through 3rd party print hosts");
def->mode = comAdvanced;
def->cli = ConfigOptionDef::nocli;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("print_host", coString); def = this->add("print_host", coString);
def->label = L("Hostname, IP or URL"); def->label = L("Hostname, IP or URL");
def->tooltip = L("Orca Slicer can upload G-code files to a printer host. This field should contain " def->tooltip = L("Orca Slicer can upload G-code files to a printer host. This field should contain "
@ -3068,6 +3076,7 @@ def = this->add("filament_loading_speed", coFloats);
def->enum_values.push_back("mks"); def->enum_values.push_back("mks");
def->enum_values.push_back("obico"); def->enum_values.push_back("obico");
def->enum_values.push_back("flashforge"); def->enum_values.push_back("flashforge");
def->enum_values.push_back("simplyprint");
def->enum_labels.push_back("PrusaLink"); def->enum_labels.push_back("PrusaLink");
def->enum_labels.push_back("PrusaConnect"); def->enum_labels.push_back("PrusaConnect");
def->enum_labels.push_back("Octo/Klipper"); def->enum_labels.push_back("Octo/Klipper");
@ -3078,6 +3087,7 @@ def = this->add("filament_loading_speed", coFloats);
def->enum_labels.push_back("MKS"); def->enum_labels.push_back("MKS");
def->enum_labels.push_back("Obico"); def->enum_labels.push_back("Obico");
def->enum_labels.push_back("Flashforge"); def->enum_labels.push_back("Flashforge");
def->enum_labels.push_back("SimplyPrint");
def->mode = comAdvanced; def->mode = comAdvanced;
def->cli = ConfigOptionDef::nocli; def->cli = ConfigOptionDef::nocli;
def->set_default_value(new ConfigOptionEnum<PrintHostType>(htOctoPrint)); def->set_default_value(new ConfigOptionEnum<PrintHostType>(htOctoPrint));

View file

@ -59,7 +59,7 @@ enum class FuzzySkinType {
}; };
enum PrintHostType { enum PrintHostType {
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htObico, htFlashforge htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htObico, htFlashforge, htSimplyPrint
}; };
enum AuthorizationType { enum AuthorizationType {

View file

@ -532,6 +532,13 @@ set(SLIC3R_GUI_SOURCES
Utils/Obico.hpp Utils/Obico.hpp
Utils/Flashforge.cpp Utils/Flashforge.cpp
Utils/Flashforge.hpp Utils/Flashforge.hpp
GUI/OAuthDialog.cpp
GUI/OAuthDialog.hpp
GUI/Jobs/OAuthJob.cpp
GUI/Jobs/OAuthJob.hpp
Utils/SimplyPrint.cpp
Utils/SimplyPrint.hpp
) )
if (WIN32) if (WIN32)

View file

@ -7,40 +7,184 @@
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
static std::string parse_params(std::string url, std::string key) std::string url_get_param(const std::string& url, const std::string& key)
{ {
size_t start = url.find(key); size_t start = url.find(key);
if (start < 0) return ""; if (start == std::string::npos) return "";
size_t eq = url.find('=', start); size_t eq = url.find('=', start);
if (eq < 0) return ""; if (eq == std::string::npos) return "";
std::string key_str = url.substr(start, eq - start); std::string key_str = url.substr(start, eq - start);
if (key_str != key) if (key_str != key)
return ""; return "";
start += key.size() + 1; start += key.size() + 1;
size_t end = url.find('&', start); size_t end = url.find('&', start);
if (end < 0) if (end == std::string::npos) end = url.length(); // Last param
return "";
std::string result = url.substr(start, end - start); std::string result = url.substr(start, end - start);
return result; return result;
} }
std::string http_headers::get_response() void session::start()
{
read_first_line();
}
void session::stop()
{
boost::system::error_code ignored_ec;
socket.shutdown(boost::asio::socket_base::shutdown_both, ignored_ec);
socket.close(ignored_ec);
}
void session::read_first_line()
{
auto self(shared_from_this());
async_read_until(socket, buff, '\r', [this, self](const boost::beast::error_code& e, std::size_t s) {
if (!e) {
std::string line, ignore;
std::istream stream{&buff};
std::getline(stream, line, '\r');
std::getline(stream, ignore, '\n');
headers.on_read_request_line(line);
read_next_line();
} else if (e != boost::asio::error::operation_aborted) {
server.stop(self);
}
});
}
void session::read_body()
{
auto self(shared_from_this());
int nbuffer = 1000;
std::shared_ptr<std::vector<char>> bufptr = std::make_shared<std::vector<char>>(nbuffer);
async_read(socket, boost::asio::buffer(*bufptr, nbuffer),
[this, self](const boost::beast::error_code& e, std::size_t s) { server.stop(self); });
}
void session::read_next_line()
{
auto self(shared_from_this());
async_read_until(socket, buff, '\r', [this, self](const boost::beast::error_code& e, std::size_t s) {
if (!e) {
std::string line, ignore;
std::istream stream{&buff};
std::getline(stream, line, '\r');
std::getline(stream, ignore, '\n');
headers.on_read_header(line);
if (line.length() == 0) {
if (headers.content_length() == 0) {
const std::string url_str = Http::url_decode(headers.get_url());
const auto resp = server.server.m_request_handler(url_str);
std::stringstream ssOut;
resp->write_response(ssOut);
std::shared_ptr<std::string> str = std::make_shared<std::string>(ssOut.str());
async_write(socket, boost::asio::buffer(str->c_str(), str->length()),
[this, self](const boost::beast::error_code& e, std::size_t s) {
std::cout << "done" << std::endl;
server.stop(self);
});
} else {
read_body();
}
} else {
read_next_line();
}
} else if (e != boost::asio::error::operation_aborted) {
server.stop(self);
}
});
}
void HttpServer::IOServer::do_accept()
{
acceptor.async_accept([this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) {
if (!acceptor.is_open()) {
return;
}
if (!ec) {
const auto ss = std::make_shared<session>(*this, std::move(socket));
start(ss);
}
do_accept();
});
}
void HttpServer::IOServer::start(std::shared_ptr<session> session)
{
sessions.insert(session);
session->start();
}
void HttpServer::IOServer::stop(std::shared_ptr<session> session)
{
sessions.erase(session);
session->stop();
}
void HttpServer::IOServer::stop_all()
{
for (auto s : sessions) {
s->stop();
}
sessions.clear();
}
HttpServer::HttpServer(boost::asio::ip::port_type port) : port(port) {}
void HttpServer::start()
{
BOOST_LOG_TRIVIAL(info) << "start_http_service...";
start_http_server = true;
m_http_server_thread = create_thread([this] {
set_current_thread_name("http_server");
server_ = std::make_unique<IOServer>(*this);
server_->acceptor.listen();
server_->do_accept();
server_->io_service.run();
});
}
void HttpServer::stop()
{
start_http_server = false;
if (server_) {
server_->acceptor.close();
server_->stop_all();
}
if (m_http_server_thread.joinable())
m_http_server_thread.join();
server_.reset();
}
void HttpServer::set_request_handler(const std::function<std::shared_ptr<Response>(const std::string&)>& request_handler)
{
this->m_request_handler = request_handler;
}
std::shared_ptr<HttpServer::Response> HttpServer::bbl_auth_handle_request(const std::string& url)
{ {
BOOST_LOG_TRIVIAL(info) << "thirdparty_login: get_response"; BOOST_LOG_TRIVIAL(info) << "thirdparty_login: get_response";
std::stringstream ssOut;
std::string url_str = Http::url_decode(url); if (boost::contains(url, "access_token")) {
if (boost::contains(url_str, "access_token")) { std::string redirect_url = url_get_param(url, "redirect_url");
std::string sHTML = "<html><body><p>redirect to url </p></body></html>"; std::string access_token = url_get_param(url, "access_token");
std::string redirect_url = parse_params(url_str, "redirect_url"); std::string refresh_token = url_get_param(url, "refresh_token");
std::string access_token = parse_params(url_str, "access_token"); std::string expires_in_str = url_get_param(url, "expires_in");
std::string refresh_token = parse_params(url_str, "refresh_token"); std::string refresh_expires_in_str = url_get_param(url, "refresh_expires_in");
std::string expires_in_str = parse_params(url_str, "expires_in"); NetworkAgent* agent = wxGetApp().getAgent();
std::string refresh_expires_in_str = parse_params(url_str, "refresh_expires_in");
NetworkAgent* agent = wxGetApp().getAgent();
unsigned int http_code; unsigned int http_code;
std::string http_body; std::string http_body;
int result = agent->get_my_profile(access_token, &http_code, &http_body); int result = agent->get_my_profile(access_token, &http_code, &http_body);
if (result == 0) { if (result == 0) {
std::string user_id; std::string user_id;
std::string user_name; std::string user_name;
@ -60,91 +204,50 @@ std::string http_headers::get_response()
; ;
} }
json j; json j;
j["data"]["refresh_token"] = refresh_token; j["data"]["refresh_token"] = refresh_token;
j["data"]["token"] = access_token; j["data"]["token"] = access_token;
j["data"]["expires_in"] = expires_in_str; j["data"]["expires_in"] = expires_in_str;
j["data"]["refresh_expires_in"] = refresh_expires_in_str; j["data"]["refresh_expires_in"] = refresh_expires_in_str;
j["data"]["user"]["uid"] = user_id; j["data"]["user"]["uid"] = user_id;
j["data"]["user"]["name"] = user_name; j["data"]["user"]["name"] = user_name;
j["data"]["user"]["account"] = user_account; j["data"]["user"]["account"] = user_account;
j["data"]["user"]["avatar"] = user_avatar; j["data"]["user"]["avatar"] = user_avatar;
agent->change_user(j.dump()); agent->change_user(j.dump());
if (agent->is_user_login()) { if (agent->is_user_login()) {
wxGetApp().request_user_login(1); wxGetApp().request_user_login(1);
} }
GUI::wxGetApp().CallAfter([this] { GUI::wxGetApp().CallAfter([] { wxGetApp().ShowUserLogin(false); });
wxGetApp().ShowUserLogin(false); std::string location_str = (boost::format("%1%?result=success") % redirect_url).str();
}); return std::make_shared<ResponseRedirect>(location_str);
std::string location_str = (boost::format("Location: %1%?result=success") % redirect_url).str();
ssOut << "HTTP/1.1 302 Found" << std::endl;
ssOut << location_str << std::endl;
ssOut << "content-type: text/html" << std::endl;
ssOut << "content-length: " << sHTML.length() << std::endl;
ssOut << std::endl;
ssOut << sHTML;
} else { } else {
std::string error_str = "get_user_profile_error_" + std::to_string(result); std::string error_str = "get_user_profile_error_" + std::to_string(result);
std::string location_str = (boost::format("Location: %1%?result=fail&error=%2%") % redirect_url % error_str).str(); std::string location_str = (boost::format("%1%?result=fail&error=%2%") % redirect_url % error_str).str();
ssOut << "HTTP/1.1 302 Found" << std::endl; return std::make_shared<ResponseRedirect>(location_str);
ssOut << location_str << std::endl;
ssOut << "content-type: text/html" << std::endl;
ssOut << "content-length: " << sHTML.length() << std::endl;
ssOut << std::endl;
ssOut << sHTML;
} }
} else { } else {
std::string sHTML = "<html><body><h1>404 Not Found</h1><p>There's nothing here.</p></body></html>"; return std::make_shared<ResponseNotFound>();
ssOut << "HTTP/1.1 404 Not Found" << std::endl;
ssOut << "content-type: text/html" << std::endl;
ssOut << "content-length: " << sHTML.length() << std::endl;
ssOut << std::endl;
ssOut << sHTML;
} }
return ssOut.str();
} }
void HttpServer::ResponseNotFound::write_response(std::stringstream& ssOut)
void accept_and_run(boost::asio::ip::tcp::acceptor& acceptor, boost::asio::io_service& io_service)
{ {
std::shared_ptr<session> sesh = std::make_shared<session>(io_service); const std::string sHTML = "<html><body><h1>404 Not Found</h1><p>There's nothing here.</p></body></html>";
acceptor.async_accept(sesh->socket, ssOut << "HTTP/1.1 404 Not Found" << std::endl;
[sesh, &acceptor, &io_service](const boost::beast::error_code& accept_error) ssOut << "content-type: text/html" << std::endl;
{ ssOut << "content-length: " << sHTML.length() << std::endl;
accept_and_run(acceptor, io_service); ssOut << std::endl;
if (!accept_error) ssOut << sHTML;
{
session::interact(sesh);
}
});
} }
HttpServer::HttpServer() void HttpServer::ResponseRedirect::write_response(std::stringstream& ssOut)
{ {
; const std::string sHTML = "<html><body><p>redirect to url </p></body></html>";
} ssOut << "HTTP/1.1 302 Found" << std::endl;
ssOut << "Location: " << location_str << std::endl;
void HttpServer::start() ssOut << "content-type: text/html" << std::endl;
{ ssOut << "content-length: " << sHTML.length() << std::endl;
BOOST_LOG_TRIVIAL(info) << "start_http_service..."; ssOut << std::endl;
start_http_server = true; ssOut << sHTML;
m_http_server_thread = Slic3r::create_thread(
[this] {
boost::asio::io_service io_service;
boost::asio::ip::tcp::endpoint endpoint{ boost::asio::ip::tcp::v4(), LOCALHOST_PORT};
boost::asio::ip::tcp::acceptor acceptor { io_service, endpoint};
acceptor.listen();
accept_and_run(acceptor, io_service);
while (start_http_server) {
io_service.run();
}
});
}
void HttpServer::stop()
{
start_http_server = false;
if (m_http_server_thread.joinable())
m_http_server_thread.join();
} }
} // GUI } // GUI

View file

@ -13,14 +13,12 @@
#include <string> #include <string>
#include <memory> #include <memory>
using namespace boost::system;
using namespace boost::asio;
#define LOCALHOST_PORT 13618 #define LOCALHOST_PORT 13618
#define LOCALHOST_URL "http://localhost:" #define LOCALHOST_URL "http://localhost:"
namespace Slic3r { namespace Slic3r { namespace GUI {
namespace GUI {
class session;
class http_headers class http_headers
{ {
@ -31,16 +29,14 @@ class http_headers
std::map<std::string, std::string> headers; std::map<std::string, std::string> headers;
public: public:
std::string get_url() { return url; }
std::string get_response();
int content_length() int content_length()
{ {
auto request = headers.find("content-length"); auto request = headers.find("content-length");
if (request != headers.end()) if (request != headers.end()) {
{
std::stringstream ssLength(request->second); std::stringstream ssLength(request->second);
int content_length; int content_length;
ssLength >> content_length; ssLength >> content_length;
return content_length; return content_length;
} }
@ -49,10 +45,10 @@ public:
void on_read_header(std::string line) void on_read_header(std::string line)
{ {
//std::cout << "header: " << line << std::endl; // std::cout << "header: " << line << std::endl;
std::stringstream ssHeader(line); std::stringstream ssHeader(line);
std::string headerName; std::string headerName;
std::getline(ssHeader, headerName, ':'); std::getline(ssHeader, headerName, ':');
std::string value; std::string value;
@ -71,92 +67,92 @@ public:
} }
}; };
class session class HttpServer
{ {
boost::asio::ip::port_type port;
public:
class Response
{
public:
virtual ~Response() = default;
virtual void write_response(std::stringstream& ssOut) = 0;
};
class ResponseNotFound : public Response
{
public:
~ResponseNotFound() override = default;
void write_response(std::stringstream& ssOut) override;
};
class ResponseRedirect : public Response
{
const std::string location_str;
public:
ResponseRedirect(const std::string& location) : location_str(location) {}
~ResponseRedirect() override = default;
void write_response(std::stringstream& ssOut) override;
};
HttpServer(boost::asio::ip::port_type port = LOCALHOST_PORT);
boost::thread m_http_server_thread;
bool start_http_server = false;
bool is_started() { return start_http_server; }
void start();
void stop();
void set_request_handler(const std::function<std::shared_ptr<Response>(const std::string&)>& m_request_handler);
static std::shared_ptr<Response> bbl_auth_handle_request(const std::string& url);
private:
class IOServer
{
public:
HttpServer& server;
boost::asio::io_service io_service;
boost::asio::ip::tcp::acceptor acceptor;
std::set<std::shared_ptr<session>> sessions;
IOServer(HttpServer& server) : server(server), acceptor(io_service, {boost::asio::ip::tcp::v4(), server.port}) {}
void do_accept();
void start(std::shared_ptr<session> session);
void stop(std::shared_ptr<session> session);
void stop_all();
};
friend class session;
std::unique_ptr<IOServer> server_{nullptr};
std::function<std::shared_ptr<Response>(const std::string&)> m_request_handler{&HttpServer::bbl_auth_handle_request};
};
class session : public std::enable_shared_from_this<session>
{
HttpServer::IOServer& server;
boost::asio::ip::tcp::socket socket;
boost::asio::streambuf buff; boost::asio::streambuf buff;
http_headers headers; http_headers headers;
static void read_body(std::shared_ptr<session> pThis) void read_first_line();
{ void read_next_line();
int nbuffer = 1000; void read_body();
std::shared_ptr<std::vector<char>> bufptr = std::make_shared<std::vector<char>>(nbuffer);
boost::asio::async_read(pThis->socket, boost::asio::buffer(*bufptr, nbuffer), [pThis](const boost::beast::error_code& e, std::size_t s)
{
});
}
static void read_next_line(std::shared_ptr<session> pThis)
{
boost::asio::async_read_until(pThis->socket, pThis->buff, '\r', [pThis](const boost::beast::error_code& e, std::size_t s)
{
std::string line, ignore;
std::istream stream{ &pThis->buff };
std::getline(stream, line, '\r');
std::getline(stream, ignore, '\n');
pThis->headers.on_read_header(line);
if (line.length() == 0)
{
if (pThis->headers.content_length() == 0)
{
std::shared_ptr<std::string> str = std::make_shared<std::string>(pThis->headers.get_response());
boost::asio::async_write(pThis->socket, boost::asio::buffer(str->c_str(), str->length()), [pThis, str](const boost::beast::error_code& e, std::size_t s)
{
std::cout << "done" << std::endl;
});
}
else
{
pThis->read_body(pThis);
}
}
else
{
pThis->read_next_line(pThis);
}
});
}
static void read_first_line(std::shared_ptr<session> pThis)
{
boost::asio::async_read_until(pThis->socket, pThis->buff, '\r', [pThis](const boost::beast::error_code& e, std::size_t s)
{
std::string line, ignore;
std::istream stream{ &pThis->buff };
std::getline(stream, line, '\r');
std::getline(stream, ignore, '\n');
pThis->headers.on_read_request_line(line);
pThis->read_next_line(pThis);
});
}
public: public:
boost::asio::ip::tcp::socket socket; session(HttpServer::IOServer& server, boost::asio::ip::tcp::socket socket) : server(server), socket(std::move(socket)) {}
session(io_service& io_service) void start();
:socket(io_service) void stop();
{
}
static void interact(std::shared_ptr<session> pThis)
{
read_first_line(pThis);
}
}; };
class HttpServer { std::string url_get_param(const std::string& url, const std::string& key);
public:
HttpServer();
boost::thread m_http_server_thread; }};
bool start_http_server = false;
bool is_started() { return start_http_server; }
void start();
void stop();
};
}
};
#endif #endif

View file

@ -0,0 +1,124 @@
#include "OAuthJob.hpp"
#include "Http.hpp"
#include "ThreadSafeQueue.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "nlohmann/json.hpp"
namespace Slic3r {
namespace GUI {
wxDEFINE_EVENT(EVT_OAUTH_COMPLETE_MESSAGE, wxCommandEvent);
OAuthJob::OAuthJob(const OAuthData& input) : local_authorization_server(input.params.callback_port), _data(input) {}
void OAuthJob::parse_token_response(const std::string& body, bool error, OAuthResult& result)
{
const auto j = nlohmann::json::parse(body, nullptr, false, true);
if (j.is_discarded()) {
BOOST_LOG_TRIVIAL(warning) << "Invalid or no JSON data on token response: " << body;
result.error_message = _u8L("Unknown error");
} else if (error) {
if (j.contains("error_description")) {
j.at("error_description").get_to(result.error_message);
} else {
result.error_message = _u8L("Unknown error");
}
} else {
j.at("access_token").get_to(result.access_token);
j.at("refresh_token").get_to(result.refresh_token);
result.success = true;
}
}
void OAuthJob::process(Ctl& ctl)
{
// Prepare auth process
ThreadSafeQueueSPSC<OAuthResult> queue;
// Setup auth server to receive OAuth code from callback url
local_authorization_server.set_request_handler([this, &queue](const std::string& url) -> std::shared_ptr<HttpServer::Response> {
if (boost::contains(url, "/callback")) {
const auto code = url_get_param(url, "code");
const auto state = url_get_param(url, "state");
const auto handle_auth_fail = [this, &queue](const std::string& message) -> std::shared_ptr<HttpServer::ResponseRedirect> {
queue.push(OAuthResult{false, message});
return std::make_shared<HttpServer::ResponseRedirect>(this->_data.params.auth_fail_redirect_url);
};
if (state != _data.params.state) {
BOOST_LOG_TRIVIAL(warning) << "The provided state was not correct. Got " << state << " and expected " << _data.params.state;
return handle_auth_fail(_u8L("The provided state is not correct."));
}
if (code.empty()) {
const auto error_code = url_get_param(url, "error_code");
if (error_code == "user_denied") {
BOOST_LOG_TRIVIAL(debug) << "User did not give the required permission when authorizing this application";
return handle_auth_fail(_u8L("Please give the required permissions when authorizing this application."));
}
BOOST_LOG_TRIVIAL(warning) << "Unexpected error when logging in. Error_code: " << error_code << ", State: " << state;
return handle_auth_fail(_u8L("Something unexpected happened when trying to log in, please try again."));
}
OAuthResult r;
// Request the access token from the authorization server.
auto http = Http::post(_data.params.token_url);
http.timeout_connect(5)
.timeout_max(5)
.form_add("client_id", _data.params.client_id)
.form_add("redirect_uri", _data.params.callback_url)
.form_add("grant_type", "authorization_code")
.form_add("code", code)
.form_add("code_verifier", _data.params.verification_code)
.form_add("scope", _data.params.scope)
.on_complete([&](std::string body, unsigned status) { parse_token_response(body, false, r); })
.on_error([&](std::string body, std::string error, unsigned status) { parse_token_response(body, true, r); })
.perform_sync();
queue.push(r);
return std::make_shared<HttpServer::ResponseRedirect>(r.success ? _data.params.auth_success_redirect_url :
_data.params.auth_fail_redirect_url);
} else {
queue.push(OAuthResult{false});
return std::make_shared<HttpServer::ResponseNotFound>();
}
});
// Run the local server
local_authorization_server.start();
// Wait until we received the result
bool received = false;
while (!ctl.was_canceled() && !received ) {
queue.consume_one(BlockingWait{1000}, [this, &received](const OAuthResult& result) {
*_data.result = result;
received = true;
});
}
// Handle timeout
if (!received && !ctl.was_canceled()) {
BOOST_LOG_TRIVIAL(debug) << "Timeout when authenticating with the account server.";
_data.result->error_message = _u8L("Timeout when authenticating with the account server.");
} else if (ctl.was_canceled()) {
_data.result->error_message = _u8L("User cancelled.");
}
}
void OAuthJob::finalize(bool canceled, std::exception_ptr& e)
{
// Make sure it's stopped
local_authorization_server.stop();
wxCommandEvent event(EVT_OAUTH_COMPLETE_MESSAGE);
event.SetEventObject(m_event_handle);
wxPostEvent(m_event_handle, event);
}
}} // namespace Slic3r::GUI

View file

@ -0,0 +1,62 @@
#ifndef __OAuthJob_HPP__
#define __OAuthJob_HPP__
#include "Job.hpp"
#include "slic3r/GUI/HttpServer.hpp"
namespace Slic3r {
namespace GUI {
class Plater;
struct OAuthParams
{
std::string login_url;
std::string client_id;
boost::asio::ip::port_type callback_port;
std::string callback_url;
std::string scope;
std::string response_type;
std::string auth_success_redirect_url;
std::string auth_fail_redirect_url;
std::string token_url;
std::string verification_code;
std::string state;
};
struct OAuthResult
{
bool success{false};
std::string error_message{""};
std::string access_token{""};
std::string refresh_token{""};
};
struct OAuthData
{
OAuthParams params;
std::shared_ptr<OAuthResult> result;
};
class OAuthJob : public Job
{
HttpServer local_authorization_server;
OAuthData _data;
wxWindow* m_event_handle{nullptr};
public:
explicit OAuthJob(const OAuthData& input);
void process(Ctl& ctl) override;
void finalize(bool canceled, std::exception_ptr& e) override;
void set_event_handle(wxWindow* hanle) { m_event_handle = hanle; }
static void parse_token_response(const std::string& body, bool error, OAuthResult& result);
};
wxDECLARE_EVENT(EVT_OAUTH_COMPLETE_MESSAGE, wxCommandEvent);
}} // namespace Slic3r::GUI
#endif // OAUTHJOB_HPP

View file

@ -562,7 +562,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
m_print_enable = get_enable_print_status(); m_print_enable = get_enable_print_status();
m_print_btn->Enable(m_print_enable); m_print_btn->Enable(m_print_enable);
if (m_print_enable) { if (m_print_enable) {
if (wxGetApp().preset_bundle->is_bbl_vendor()) if (wxGetApp().preset_bundle->use_bbl_network())
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE)); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE));
else else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE)); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE));
@ -1600,7 +1600,7 @@ wxBoxSizer* MainFrame::create_side_tools()
SidePopup* p = new SidePopup(this); SidePopup* p = new SidePopup(this);
if (wxGetApp().preset_bundle if (wxGetApp().preset_bundle
&& !wxGetApp().preset_bundle->is_bbl_vendor()) { && !wxGetApp().preset_bundle->use_bbl_network()) {
// ThirdParty Buttons // ThirdParty Buttons
SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), ""); SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), "");
export_gcode_btn->SetCornerRadius(0); export_gcode_btn->SetCornerRadius(0);
@ -3584,7 +3584,7 @@ void MainFrame::load_printer_url(wxString url, wxString apikey)
void MainFrame::load_printer_url() void MainFrame::load_printer_url()
{ {
PresetBundle &preset_bundle = *wxGetApp().preset_bundle; PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
if (preset_bundle.is_bbl_vendor()) if (preset_bundle.use_bbl_network())
return; return;
auto cfg = preset_bundle.printers.get_edited_preset().config; auto cfg = preset_bundle.printers.get_edited_preset().config;

View file

@ -0,0 +1,84 @@
#include "OAuthDialog.hpp"
#include "GUI_App.hpp"
#include "Jobs/BoostThreadWorker.hpp"
#include "Jobs/PlaterWorker.hpp"
#include "wxExtensions.hpp"
namespace Slic3r {
namespace GUI {
#define BORDER_W FromDIP(10)
OAuthDialog::OAuthDialog(wxWindow* parent, OAuthParams params)
: DPIDialog(parent, wxID_ANY, _L("Login"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), -1), wxDEFAULT_DIALOG_STYLE)
, _params(params)
{
SetFont(wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE);
m_worker = std::make_unique<PlaterWorker<BoostThreadWorker>>(this, nullptr, "auth_worker");
wxStdDialogButtonSizer* btns = this->CreateStdDialogButtonSizer(wxCANCEL);
btnCancel = static_cast<wxButton*>(this->FindWindowById(wxID_CANCEL, this));
wxGetApp().UpdateDarkUI(btnCancel);
btnCancel->Bind(wxEVT_BUTTON, &OAuthDialog::on_cancel, this);
const auto message_sizer = new wxBoxSizer(wxVERTICAL);
const auto message = new wxStaticText(this, wxID_ANY, _L("Authorizing..."), wxDefaultPosition, wxDefaultSize, 0);
message->SetForegroundColour(*wxBLACK);
message_sizer->Add(message, 0, wxEXPAND | wxLEFT | wxTOP | wxBOTTOM, BORDER_W);
const auto topSizer = new wxBoxSizer(wxVERTICAL);
topSizer->Add(message_sizer, 0, wxEXPAND | wxALL, BORDER_W);
topSizer->Add(btns, 0, wxEXPAND | wxALL, BORDER_W);
Bind(wxEVT_CLOSE_WINDOW, &OAuthDialog::on_cancel, this);
SetSizer(topSizer);
topSizer->SetSizeHints(this);
this->CenterOnParent();
wxGetApp().UpdateDlgDarkUI(this);
}
void OAuthDialog::on_cancel(wxEvent& event)
{
m_worker->cancel_all();
m_worker->wait_for_idle();
EndModal(wxID_NO);
}
bool OAuthDialog::Show(bool show)
{
if (show) {
// Prepare login job
_result = std::make_shared<OAuthResult>();
auto job = std::make_unique<OAuthJob>(OAuthData{_params, _result});
job->set_event_handle(this);
Bind(EVT_OAUTH_COMPLETE_MESSAGE, [this](wxCommandEvent& evt) { EndModal(wxID_NO); });
// Start auth job
replace_job(*m_worker, std::move(job));
// Open login URL in external browser
wxLaunchDefaultBrowser(_params.login_url);
}
return DPIDialog::Show(show);
}
void OAuthDialog::on_dpi_changed(const wxRect& suggested_rect)
{
const int& em = em_unit();
msw_buttons_rescale(this, em, {wxID_CANCEL});
const wxSize& size = wxSize(45 * em, 35 * em);
SetMinSize(size);
Fit();
Refresh();
}
}}

View file

@ -0,0 +1,34 @@
#ifndef __OAuthDialog_HPP__
#define __OAuthDialog_HPP__
#include "GUI_Utils.hpp"
#include "Jobs/OAuthJob.hpp"
#include "Jobs/Worker.hpp"
namespace Slic3r {
namespace GUI {
class OAuthDialog : public DPIDialog
{
private:
OAuthParams _params;
std::shared_ptr<OAuthResult> _result;
wxButton* btnCancel{nullptr};
std::unique_ptr<Worker> m_worker;
void on_cancel(wxEvent& event);
protected:
bool Show(bool show) override;
void on_dpi_changed(const wxRect& suggested_rect) override;
public:
OAuthDialog(wxWindow* parent, OAuthParams params);
OAuthResult get_result() { return *_result; }
};
}} // namespace Slic3r::GUI
#endif

View file

@ -194,6 +194,13 @@ void OptionsGroup::show_field(const t_config_option_key& opt_key, bool show/* =
} }
} }
void OptionsGroup::enable_field(const t_config_option_key& opt_key, bool enable)
{
if (Field* f = get_field(opt_key); f) {
f->toggle(enable);
}
}
void OptionsGroup::set_name(const wxString& new_name) void OptionsGroup::set_name(const wxString& new_name)
{ {
stb->SetLabel(new_name); stb->SetLabel(new_name);

View file

@ -173,6 +173,9 @@ public:
void show_field(const t_config_option_key& opt_key, bool show = true); void show_field(const t_config_option_key& opt_key, bool show = true);
void hide_field(const t_config_option_key& opt_key) { show_field(opt_key, false); } void hide_field(const t_config_option_key& opt_key) { show_field(opt_key, false); }
void enable_field(const t_config_option_key& opt_key, bool enable = true);
void disable_field(const t_config_option_key& opt_key) { enable_field(opt_key, false); }
void set_name(const wxString& new_name); void set_name(const wxString& new_name);
inline void enable() { for (auto& field : m_fields) field.second->enable(); } inline void enable() { for (auto& field : m_fields) field.second->enable(); }

View file

@ -34,6 +34,8 @@
#include "BitmapCache.hpp" #include "BitmapCache.hpp"
#include "BonjourDialog.hpp" #include "BonjourDialog.hpp"
#include "MsgDialog.hpp" #include "MsgDialog.hpp"
#include "OAuthDialog.hpp"
#include "SimplyPrint.hpp"
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
@ -174,20 +176,24 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
result = host->test(msg); result = host->test(msg);
if (!result && host->is_cloud()) { if (!result && host->is_cloud()) {
PrinterCloudAuthDialog dlg(this->GetParent(), host.get()); if (const auto h = dynamic_cast<SimplyPrint*>(host.get()); h) {
dlg.ShowModal(); OAuthDialog dlg(this, h->get_oauth_params());
dlg.ShowModal();
auto api_key = dlg.GetApiKey(); const auto& r = dlg.get_result();
m_config->opt_string("printhost_apikey") = api_key; result = r.success;
result = !api_key.empty(); if (r.success) {
if (result) { h->save_oauth_credential(r);
if (Field* print_host_webui_field = this->m_optgroup->get_field("printhost_apikey"); print_host_webui_field) { } else {
if (TextInput* temp_input = dynamic_cast<TextInput*>(print_host_webui_field->getWindow()); temp_input) { msg = r.error_message;
if (wxTextCtrl* temp = temp_input->GetTextCtrl()) {
temp->SetValue(wxString(api_key));
}
}
} }
} else {
PrinterCloudAuthDialog dlg(this->GetParent(), host.get());
dlg.ShowModal();
const auto api_key = dlg.GetApiKey();
m_config->opt_string("printhost_apikey") = api_key;
result = !api_key.empty();
} }
} }
} }
@ -195,11 +201,36 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
show_info(this, host->get_test_ok_msg(), _L("Success!")); show_info(this, host->get_test_ok_msg(), _L("Success!"));
else else
show_error(this, host->get_test_failed_msg(msg)); show_error(this, host->get_test_failed_msg(msg));
update();
}); });
return sizer; return sizer;
}; };
auto print_host_logout = [&](wxWindow* parent) {
auto sizer = create_sizer_with_btn(parent, &m_printhost_logout_btn, "", _L("Log Out"));
m_printhost_logout_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
std::unique_ptr<PrintHost> host(PrintHost::get_print_host(m_config));
if (!host) {
const wxString text = _L("Could not get a valid Printer Host reference");
show_error(this, text);
return;
}
wxString msg_text = _L("Are you sure to log out?");
MessageDialog dialog(this, msg_text, "", wxICON_QUESTION | wxYES_NO);
if (dialog.ShowModal() == wxID_YES) {
host->log_out();
update();
}
});
return sizer;
};
auto print_host_printers = [this, create_sizer_with_btn](wxWindow* parent) { auto print_host_printers = [this, create_sizer_with_btn](wxWindow* parent) {
//add_scaled_button(parent, &m_printhost_port_browse_btn, "browse", _(L("Refresh Printers")), wxBU_LEFT | wxBU_EXACTFIT); //add_scaled_button(parent, &m_printhost_port_browse_btn, "browse", _(L("Refresh Printers")), wxBU_LEFT | wxBU_EXACTFIT);
auto sizer = create_sizer_with_btn(parent, &m_printhost_port_browse_btn, "monitor_signal_strong", _(L("Refresh Printers"))); auto sizer = create_sizer_with_btn(parent, &m_printhost_port_browse_btn, "monitor_signal_strong", _(L("Refresh Printers")));
@ -215,6 +246,7 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
Line host_line = m_optgroup->create_single_option_line(option); Line host_line = m_optgroup->create_single_option_line(option);
host_line.append_widget(printhost_browse); host_line.append_widget(printhost_browse);
host_line.append_widget(print_host_test); host_line.append_widget(print_host_test);
host_line.append_widget(print_host_logout);
m_optgroup->append_line(host_line); m_optgroup->append_line(host_line);
option = m_optgroup->get_option("print_host_webui"); option = m_optgroup->get_option("print_host_webui");
@ -375,7 +407,9 @@ void PhysicalPrinterDialog::update_printhost_buttons()
std::unique_ptr<PrintHost> host(PrintHost::get_print_host(m_config)); std::unique_ptr<PrintHost> host(PrintHost::get_print_host(m_config));
if (host) { if (host) {
m_printhost_test_btn->Enable(!m_config->opt_string("print_host").empty() && host->can_test()); m_printhost_test_btn->Enable(!m_config->opt_string("print_host").empty() && host->can_test());
m_printhost_browse_btn->Enable(host->has_auto_discovery()); m_printhost_browse_btn->Show(host->has_auto_discovery());
m_printhost_logout_btn->Show(host->is_logged_in());
m_printhost_test_btn->SetLabel(host->is_cloud() ? _L("Login/Test") : _L("Test"));
} }
} }
@ -469,15 +503,29 @@ void PhysicalPrinterDialog::update(bool printer_change)
const auto opt = m_config->option<ConfigOptionEnum<PrintHostType>>("host_type"); const auto opt = m_config->option<ConfigOptionEnum<PrintHostType>>("host_type");
m_optgroup->show_field("host_type"); m_optgroup->show_field("host_type");
// hide PrusaConnect address m_optgroup->enable_field("print_host");
if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { m_optgroup->enable_field("print_host_webui");
if (wxTextCtrl* temp = dynamic_cast<wxTextCtrl*>(printhost_field->getWindow()); temp && temp->GetValue() == L"https://connect.prusa3d.com") { m_optgroup->enable_field("printhost_cafile");
temp->SetValue(wxString()); m_optgroup->enable_field("printhost_ssl_ignore_revoke");
} if (m_printhost_cafile_browse_btn)
m_printhost_cafile_browse_btn->Enable();
if (TextInput* temp_input = dynamic_cast<TextInput*>(printhost_field->getWindow()); temp_input) { // hide pre-configured address, in case user switched to a different host type
if (wxTextCtrl* temp = temp_input->GetTextCtrl(); temp &&temp->GetValue() == L"https://app.obico.io") { if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
temp->SetValue(wxString()); if (wxTextCtrl* temp = dynamic_cast<TextCtrl*>(printhost_field)->text_ctrl(); temp) {
const auto current_host = temp->GetValue();
if (current_host == L"https://connect.prusa3d.com" ||
current_host == L"https://app.obico.io" ||
current_host == "https://simplyprint.io") {
temp->SetValue(wxString());
}
}
}
if (Field* printhost_webui_field = m_optgroup->get_field("print_host_webui"); printhost_webui_field) {
if (wxTextCtrl* temp = dynamic_cast<TextCtrl*>(printhost_webui_field)->text_ctrl(); temp) {
const auto current_host = temp->GetValue();
if (current_host == "https://simplyprint.io/panel") {
temp->SetValue(wxString());
} }
} }
} }
@ -492,25 +540,42 @@ void PhysicalPrinterDialog::update(bool printer_change)
m_optgroup->show_field("printhost_apikey", true); m_optgroup->show_field("printhost_apikey", true);
for (const std::string& opt_key : std::vector<std::string>{ "printhost_user", "printhost_password" }) for (const std::string& opt_key : std::vector<std::string>{ "printhost_user", "printhost_password" })
m_optgroup->hide_field(opt_key); m_optgroup->hide_field(opt_key);
supports_multiple_printers = opt && opt->value == htRepetier; supports_multiple_printers = opt->value == htRepetier || opt->value == htObico;
if (opt->value == htPrusaConnect) { // automatically show default prusaconnect address if (opt->value == htPrusaConnect) { // automatically show default prusaconnect address
if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
if (wxTextCtrl* temp = dynamic_cast<wxTextCtrl*>(printhost_field->getWindow()); temp && temp->GetValue().IsEmpty()) { if (wxTextCtrl* temp = dynamic_cast<TextCtrl*>(printhost_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) {
temp->SetValue(L"https://connect.prusa3d.com"); temp->SetValue(L"https://connect.prusa3d.com");
} }
} }
} } else if (opt->value == htObico) { // automatically show default obico address
} if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
if (wxTextCtrl* temp = dynamic_cast<TextCtrl*>(printhost_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) {
if (opt->value == htObico) {
supports_multiple_printers = true;
if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
if (TextInput* temp_input = dynamic_cast<TextInput*>(printhost_field->getWindow()); temp_input) {
if (wxTextCtrl* temp = temp_input->GetTextCtrl(); temp && temp->GetValue().IsEmpty()) {
temp->SetValue(L"https://app.obico.io"); temp->SetValue(L"https://app.obico.io");
m_config->opt_string("print_host") = "https://app.obico.io"; m_config->opt_string("print_host") = "https://app.obico.io";
} }
} }
} else if (opt->value == htSimplyPrint) {
// Set the host url
if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
printhost_field->disable();
if (wxTextCtrl* temp = dynamic_cast<TextCtrl*>(printhost_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) {
temp->SetValue("https://simplyprint.io");
}
m_config->opt_string("print_host") = "https://simplyprint.io";
}
if (Field* printhost_webui_field = m_optgroup->get_field("print_host_webui"); printhost_webui_field) {
printhost_webui_field->disable();
if (wxTextCtrl* temp = dynamic_cast<TextCtrl*>(printhost_webui_field)->text_ctrl(); temp && temp->GetValue().IsEmpty()) {
temp->SetValue("https://simplyprint.io/panel");
}
m_config->opt_string("print_host_webui") = "https://simplyprint.io/panel";
}
m_optgroup->hide_field("printhost_apikey");
m_optgroup->disable_field("printhost_cafile");
m_optgroup->disable_field("printhost_ssl_ignore_revoke");
if (m_printhost_cafile_browse_btn)
m_printhost_cafile_browse_btn->Disable();
} }
} }
@ -599,6 +664,7 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect)
m_printhost_browse_btn->msw_rescale(); m_printhost_browse_btn->msw_rescale();
m_printhost_test_btn->msw_rescale(); m_printhost_test_btn->msw_rescale();
m_printhost_logout_btn->msw_rescale();
if (m_printhost_cafile_browse_btn) if (m_printhost_cafile_browse_btn)
m_printhost_cafile_browse_btn->msw_rescale(); m_printhost_cafile_browse_btn->msw_rescale();

View file

@ -30,6 +30,7 @@ class PhysicalPrinterDialog : public DPIDialog
ScalableButton* m_printhost_browse_btn {nullptr}; ScalableButton* m_printhost_browse_btn {nullptr};
ScalableButton* m_printhost_test_btn {nullptr}; ScalableButton* m_printhost_test_btn {nullptr};
ScalableButton* m_printhost_logout_btn {nullptr};
ScalableButton* m_printhost_cafile_browse_btn {nullptr}; ScalableButton* m_printhost_cafile_browse_btn {nullptr};
ScalableButton* m_printhost_client_cert_browse_btn {nullptr}; ScalableButton* m_printhost_client_cert_browse_btn {nullptr};
ScalableButton* m_printhost_port_browse_btn {nullptr}; ScalableButton* m_printhost_port_browse_btn {nullptr};

View file

@ -1132,13 +1132,14 @@ void Sidebar::update_all_preset_comboboxes()
const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology();
bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); bool is_bbl_vendor = preset_bundle.is_bbl_vendor();
const bool use_bbl_network = preset_bundle.use_bbl_network();
// Orca:: show device tab based on vendor type // Orca:: show device tab based on vendor type
auto p_mainframe = wxGetApp().mainframe; auto p_mainframe = wxGetApp().mainframe;
p_mainframe->show_device(is_bbl_vendor); p_mainframe->show_device(use_bbl_network);
auto cfg = preset_bundle.printers.get_edited_preset().config; auto cfg = preset_bundle.printers.get_edited_preset().config;
if (is_bbl_vendor) { if (use_bbl_network) {
//only show connection button for not-BBL printer //only show connection button for not-BBL printer
connection_btn->Hide(); connection_btn->Hide();
//only show sync-ams button for BBL printer //only show sync-ams button for BBL printer
@ -1270,7 +1271,7 @@ void Sidebar::update_presets(Preset::Type preset_type)
} }
Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
bool isBBL = preset_bundle.is_bbl_vendor(); bool isBBL = preset_bundle.use_bbl_network();
wxGetApp().mainframe->show_calibration_button(!isBBL); wxGetApp().mainframe->show_calibration_button(!isBBL);
if (auto printer_structure_opt = printer_preset.config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure")) { if (auto printer_structure_opt = printer_preset.config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure")) {
@ -6836,7 +6837,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview; sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview;
update_sidebar(); update_sidebar();
int old_sel = e.GetOldSelection(); int old_sel = e.GetOldSelection();
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor() && new_sel == MainFrame::tpMonitor) { if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->use_bbl_network() && new_sel == MainFrame::tpMonitor) {
if (!wxGetApp().getAgent()) { if (!wxGetApp().getAgent()) {
e.Veto(); e.Veto();
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel; BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel;

View file

@ -23,7 +23,7 @@
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
PrinterCloudAuthDialog::PrinterCloudAuthDialog(wxWindow* parent, PrintHost* host) PrinterCloudAuthDialog::PrinterCloudAuthDialog(wxWindow* parent, PrintHost* host)
: wxDialog((wxWindow*) (wxGetApp().mainframe), wxID_ANY, "Login"), m_host(host) : wxDialog((wxWindow*) (wxGetApp().mainframe), wxID_ANY, "Login")
{ {
SetBackgroundColour(*wxWHITE); SetBackgroundColour(*wxWHITE);
// Url // Url
@ -91,7 +91,6 @@ void PrinterCloudAuthDialog::OnScriptMessage(wxWebViewEvent& evt)
wxString strCmd = j["command"]; wxString strCmd = j["command"];
if (strCmd == "login_token") { if (strCmd == "login_token") {
auto token = j["data"]["token"]; auto token = j["data"]["token"];
m_host->set_api_key(token);
m_apikey = token; m_apikey = token;
} }
Close(); Close();

View file

@ -29,7 +29,6 @@ protected:
wxString m_javascript; wxString m_javascript;
wxString m_response_js; wxString m_response_js;
PrintHost* m_host;
std::string m_apikey; std::string m_apikey;
public: public:

View file

@ -3546,6 +3546,7 @@ void TabPrinter::build_fff()
optgroup = page->new_optgroup(L("Advanced"), L"param_advanced"); optgroup = page->new_optgroup(L("Advanced"), L"param_advanced");
optgroup->append_single_option_line("printer_structure"); optgroup->append_single_option_line("printer_structure");
optgroup->append_single_option_line("gcode_flavor"); optgroup->append_single_option_line("gcode_flavor");
optgroup->append_single_option_line("bbl_use_printhost");
optgroup->append_single_option_line("disable_m73"); optgroup->append_single_option_line("disable_m73");
option = optgroup->get_option("thumbnails"); option = optgroup->get_option("thumbnails");
option.opt.full_width = true; option.opt.full_width = true;
@ -4188,7 +4189,7 @@ void TabPrinter::toggle_options()
// SoftFever: hide BBL specific settings // SoftFever: hide BBL specific settings
for (auto el : for (auto el :
{"scan_first_layer", "machine_load_filament_time", "machine_unload_filament_time", "bbl_calib_mark_logo"}) {"scan_first_layer", "machine_load_filament_time", "machine_unload_filament_time", "bbl_calib_mark_logo", "bbl_use_printhost"})
toggle_line(el, is_BBL_printer); toggle_line(el, is_BBL_printer);
// SoftFever: hide non-BBL settings // SoftFever: hide non-BBL settings

View file

@ -42,8 +42,6 @@ Obico::Obico(DynamicPrintConfig* config) :
const char* Obico::get_name() const { return "Obico"; } const char* Obico::get_name() const { return "Obico"; }
void Obico::set_api_key(const std::string auth_api_key) { m_apikey = auth_api_key; }
std::string Obico::get_host() const { std::string Obico::get_host() const {
return m_host; return m_host;
} }

View file

@ -24,7 +24,6 @@ public:
bool has_auto_discovery() const override { return false; } bool has_auto_discovery() const override { return false; }
bool is_cloud() const override { return true; } bool is_cloud() const override { return true; }
bool get_login_url(wxString& auth_url) const override; bool get_login_url(wxString& auth_url) const override;
void set_api_key(const std::string auth_api_key) override;
std::string get_host() const override; std::string get_host() const override;
wxString get_test_ok_msg() const override; wxString get_test_ok_msg() const override;

View file

@ -22,6 +22,7 @@
#include "../GUI/PrintHostDialogs.hpp" #include "../GUI/PrintHostDialogs.hpp"
#include "Obico.hpp" #include "Obico.hpp"
#include "Flashforge.hpp" #include "Flashforge.hpp"
#include "SimplyPrint.hpp"
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
using boost::optional; using boost::optional;
@ -58,6 +59,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config)
case htMKS: return new MKS(config); case htMKS: return new MKS(config);
case htObico: return new Obico(config); case htObico: return new Obico(config);
case htFlashforge: return new Flashforge(config); case htFlashforge: return new Flashforge(config);
case htSimplyPrint: return new SimplyPrint(config);
default: return nullptr; default: return nullptr;
} }
} else { } else {

View file

@ -72,8 +72,9 @@ public:
//Support for cloud webui login //Support for cloud webui login
virtual bool is_cloud() const { return false; } virtual bool is_cloud() const { return false; }
virtual bool is_logged_in() const { return false; }
virtual void log_out() const {}
virtual bool get_login_url(wxString& auth_url) const { return false; } virtual bool get_login_url(wxString& auth_url) const { return false; }
virtual void set_api_key(const std::string auth_api_key) {}
protected: protected:
virtual wxString format_error(const std::string &body, const std::string &error, unsigned status) const; virtual wxString format_error(const std::string &body, const std::string &error, unsigned status) const;

View file

@ -0,0 +1,296 @@
#include "SimplyPrint.hpp"
#include <openssl/sha.h>
#include <boost/beast/core/detail/base64.hpp>
#include "nlohmann/json.hpp"
#include "libslic3r/Utils.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp"
namespace Slic3r {
static constexpr boost::asio::ip::port_type CALLBACK_PORT = 21328;
static const std::string CALLBACK_URL = "http://localhost:21328/callback";
static const std::string RESPONSE_TYPE = "code";
static const std::string CLIENT_ID = "simplyprintorcaslicer";
static const std::string CLIENT_SCOPES = "user.read files.temp_upload";
static const std::string OAUTH_CREDENTIAL_PATH = "simplyprint_oauth.json";
static const std::string TOKEN_URL = "https://simplyprint.io/api/oauth2/Token";
static std::string generate_verification_code(int code_length = 32)
{
std::stringstream ss;
for (auto i = 0; i < code_length; i++) {
ss << std::hex << std::setw(2) << std::setfill('0') << (rand() % 0x100);
}
return ss.str();
}
static std::string sha256b64(const std::string& inputStr)
{
unsigned char hash[SHA256_DIGEST_LENGTH];
const unsigned char* data = (const unsigned char*) inputStr.c_str();
SHA256(data, inputStr.size(), hash);
std::string b64;
b64.resize(boost::beast::detail::base64::encoded_size(sizeof(hash)));
b64.resize(boost::beast::detail::base64::encode(&b64[0], hash, sizeof(hash)));
// uses '-' instead of '+' and '_' instead of '/' for url-safe
std::replace(b64.begin(), b64.end(), '+', '-');
std::replace(b64.begin(), b64.end(), '/', '_');
// Stripping "=" is for RFC 7636 compliance
b64.erase(std::remove(b64.begin(), b64.end(), '='), b64.end());
return b64;
}
static std::string url_encode(const std::vector<std::pair<std::string, std::string>> query)
{
std::vector<std::string> q;
q.reserve(query.size());
std::transform(query.begin(), query.end(), std::back_inserter(q), [](const auto& kv) {
return Http::url_encode(kv.first) + "=" + Http::url_encode(kv.second);
});
return boost::algorithm::join(q, "&");
}
static void set_auth(Http& http, const std::string& access_token) { http.header("Authorization", "Bearer " + access_token); }
SimplyPrint::SimplyPrint(DynamicPrintConfig* config)
{
cred_file = (boost::filesystem::path(data_dir()) / OAUTH_CREDENTIAL_PATH).make_preferred().string();
load_oauth_credential();
}
GUI::OAuthParams SimplyPrint::get_oauth_params() const
{
const auto verification_code = generate_verification_code();
// SimplyPrint uses S256 for PKCE
const auto code_challenge = sha256b64(verification_code);
const auto state = generate_verification_code();
const std::vector<std::pair<std::string, std::string>> query_parameters{
{"client_id", CLIENT_ID},
{"redirect_uri", CALLBACK_URL},
{"scope", CLIENT_SCOPES},
{"response_type", RESPONSE_TYPE},
{"state", state},
{"code_challenge", code_challenge},
{"code_challenge_method", "S256"},
};
const auto login_url = (boost::format("https://simplyprint.io/panel/oauth2/authorize?%s") % url_encode(query_parameters)).str();
return GUI::OAuthParams{
login_url,
CLIENT_ID,
CALLBACK_PORT,
CALLBACK_URL,
CLIENT_SCOPES,
RESPONSE_TYPE,
"https://simplyprint.io/login-success",
"https://simplyprint.io/login-success",
TOKEN_URL,
verification_code,
state,
};
}
void SimplyPrint::load_oauth_credential()
{
cred.clear();
if (boost::filesystem::exists(cred_file)) {
nlohmann::json j;
try {
boost::nowide::ifstream ifs(cred_file);
ifs >> j;
ifs.close();
cred["access_token"] = j["access_token"];
cred["refresh_token"] = j["refresh_token"];
} catch (std::exception& err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << cred_file << " failed, reason = " << err.what();
cred.clear();
}
}
}
void SimplyPrint::save_oauth_credential(const GUI::OAuthResult& cred) const
{
nlohmann::json j;
j["access_token"] = cred.access_token;
j["refresh_token"] = cred.refresh_token;
boost::nowide::ofstream c;
c.open(cred_file, std::ios::out | std::ios::trunc);
c << std::setw(4) << j << std::endl;
c.close();
}
wxString SimplyPrint::get_test_ok_msg() const { return _(L("Connected to SimplyPrint successfully!")); }
wxString SimplyPrint::get_test_failed_msg(wxString& msg) const
{
return GUI::format_wxstr("%s: %s", _L("Could not connect to SimplyPrint"), msg.Truncate(256));
}
void SimplyPrint::log_out() const
{
boost::nowide::remove(cred_file.c_str());
}
bool SimplyPrint::do_api_call(std::function<Http(bool)> build_request,
std::function<bool(std::string, unsigned)> on_complete,
std::function<bool(std::string, std::string, unsigned)> on_error) const
{
if (cred.find("access_token") == cred.end()) {
return false;
}
bool res = true;
const auto create_request = [this, &build_request, &res, &on_complete](const std::string& access_token, bool is_retry) {
auto http = build_request(is_retry);
set_auth(http, access_token);
http.header("User-Agent", "SimplyPrint Orca Plugin")
.on_complete([&](std::string body, unsigned http_status) {
res = on_complete(body, http_status);
});
return http;
};
create_request(cred.at("access_token"), false)
.on_error([&res, &on_error, this, &create_request](std::string body, std::string error, unsigned http_status) {
if (http_status == 401) {
// Refresh token
BOOST_LOG_TRIVIAL(warning) << boost::format("SimplyPrint: Access token invalid: %1%, HTTP %2%, body: `%3%`") % error %
http_status % body;
BOOST_LOG_TRIVIAL(info) << "SimplyPrint: Attempt to refresh access token";
auto http = Http::post(TOKEN_URL);
http.timeout_connect(5)
.timeout_max(5)
.form_add("grant_type", "refresh_token")
.form_add("client_id", CLIENT_ID)
.form_add("refresh_token", cred.at("refresh_token"))
.on_complete([this, &res, &on_error, &create_request](std::string body, unsigned http_status) {
GUI::OAuthResult r;
GUI::OAuthJob::parse_token_response(body, false, r);
if (r.success) {
BOOST_LOG_TRIVIAL(info) << "SimplyPrint: Successfully refreshed access token";
this->save_oauth_credential(r);
// Run the api call again
create_request(r.access_token, true)
.on_error([&res, &on_error](std::string body, std::string error, unsigned http_status) {
res = on_error(body, error, http_status);
})
.perform_sync();
} else {
BOOST_LOG_TRIVIAL(error)
<< boost::format("SimplyPrint: Failed to refresh access token: %1%, body: `%2%`") % r.error_message % body;
res = on_error(body, r.error_message, http_status);
}
})
.on_error([&res, &on_error](std::string body, std::string error, unsigned http_status) {
BOOST_LOG_TRIVIAL(error)
<< boost::format("SimplyPrint: Failed to refresh access token: %1%, HTTP %2%, body: `%3%`") % error %
http_status % body;
res = on_error(body, error, http_status);
})
.perform_sync();
} else {
res = on_error(body, error, http_status);
}
})
.perform_sync();
return res;
}
bool SimplyPrint::test(wxString& curl_msg) const
{
if (cred.find("access_token") == cred.end()) {
return false;
}
return do_api_call(
[](bool is_retry) {
auto http = Http::get("https://api.simplyprint.io/oauth2/TokenInfo");
http.header("Accept", "application/json");
return http;
},
[](std::string body, unsigned) {
BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: Got token info: %1%") % body;
return true;
},
[](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("SimplyPrint: Error getting token info: %1%, HTTP %2%, body: `%3%`") % error %
status % body;
return false;
});
}
bool SimplyPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const
{
if (cred.find("access_token") == cred.end()) {
error_fn(_L("SimplyPrint account not linked. Go to Connect options to set it up."));
return false;
}
// If file is over 100 MB, fail
if (boost::filesystem::file_size(upload_data.source_path) > 104857600ull) {
error_fn(_L("File size exceeds the 100MB upload limit. Please upload your file through the panel."));
return false;
}
const auto filename = upload_data.upload_path.filename().string();
return do_api_call(
[&upload_data, &prorgess_fn, &filename](bool is_retry) {
auto http = Http::post("https://simplyprint.io/api/files/TempUpload");
http.form_add_file("file", upload_data.source_path.string(), filename)
.on_progress([&prorgess_fn](Http::Progress progress, bool& cancel) { prorgess_fn(std::move(progress), cancel); });
return http;
},
[&error_fn, &filename](std::string body, unsigned status) {
BOOST_LOG_TRIVIAL(info) << boost::format("SimplyPrint: File uploaded: HTTP %1%: %2%") % status % body;
// Get file UUID
const auto j = nlohmann::json::parse(body, nullptr, false, true);
if (j.is_discarded()) {
BOOST_LOG_TRIVIAL(error) << "SimplyPrint: Invalid or no JSON data on token response: " << body;
error_fn(_L("Unknown error"));
return false;
}
if (j.find("uuid") == j.end()) {
BOOST_LOG_TRIVIAL(error) << "SimplyPrint: Invalid or no JSON data on token response: " << body;
error_fn(_L("Unknown error"));
return false;
}
const std::string uuid = j["uuid"];
// Launch external browser for file importing after uploading
const auto url = "https://simplyprint.io/panel?" + url_encode({{"import", "tmp:" + uuid}, {"filename", filename}});
wxLaunchDefaultBrowser(url);
return true;
},
[this, &error_fn](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("SimplyPrint: Error uploading file : %1%, HTTP %2%, body: `%3%`") % error % status % body;
error_fn(format_error(body, error, status));
return false;
});
}
} // namespace Slic3r

View file

@ -0,0 +1,45 @@
#ifndef slic3r_SimplyPrint_hpp_
#define slic3r_SimplyPrint_hpp_
#include "PrintHost.hpp"
#include "slic3r/GUI/Jobs/OAuthJob.hpp"
namespace Slic3r {
class DynamicPrintConfig;
class Http;
class SimplyPrint : public PrintHost
{
std::string cred_file;
std::map<std::string, std::string> cred;
void load_oauth_credential();
bool do_api_call(std::function<Http(bool /*is_retry*/)> build_request,
std::function<bool(std::string /* body */, unsigned /* http_status */)> on_complete,
std::function<bool(std::string /* body */, std::string /* error */, unsigned /* http_status */)> on_error) const;
public:
SimplyPrint(DynamicPrintConfig* config);
~SimplyPrint() override = default;
const char* get_name() const override { return "SimplyPrint"; }
bool can_test() const override { return true; }
bool has_auto_discovery() const override { return false; }
bool is_cloud() const override { return true; }
std::string get_host() const override { return "https://simplyprint.io"; }
GUI::OAuthParams get_oauth_params() const;
void save_oauth_credential(const GUI::OAuthResult& cred) const;
wxString get_test_ok_msg() const override;
wxString get_test_failed_msg(wxString& msg) const override;
bool test(wxString& curl_msg) const override;
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::QueuePrint; }
bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override;
bool is_logged_in() const override { return !cred.empty(); }
void log_out() const override;
};
} // namespace Slic3r
#endif