phoenix-firestorm/indra/newview/fsprimfeedconnect.cpp

189 lines
7.5 KiB
C++

/**
* @file fsprimfeedconnect.cpp
* @brief Primfeed connector class
* @author beq@firestorm
*
* $LicenseInfo:firstyear=2025&license=fsviewerlgpl$
* Phoenix Firestorm Viewer Source Code
* Copyright (C) 2025, Beq Janus
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
* http://www.firestormviewer.org
* $/LicenseInfo$
*/
#include "fsprimfeedconnect.h"
#include "fsprimfeedauth.h"
#include "llviewercontrol.h"
#include "llcoros.h"
#include "llsdjson.h"
// The connector workflow for Primfeed is realtively simple and mostly just builds on top of the established Auth workflow
// and the posting endpoint documented at https://docs.primfeed.com/api/third-party-viewers#creating-a-post
FSPrimfeedConnect::FSPrimfeedConnect() = default;
void FSPrimfeedConnect::uploadPhoto(const LLSD& params, LLImageFormatted* image, post_callback_t callback)
{
LL_DEBUGS("primfeed") << "uploadPhoto() called" << LL_ENDL;
if (!FSPrimfeedAuth::isAuthorized())
{
LL_WARNS("primfeed") << "Authorization failed, aborting.\n" << LL_ENDL;
callback(false, "");
return;
}
LL_DEBUGS("primfeed") << "Authorization successful" << LL_ENDL;
mPostCallback = callback;
LL_DEBUGS("primfeed") << "Launching upload coroutine" << LL_ENDL;
LLCoros::instance().launch(
"FSPrimfeedConnect::uploadPhotoCoro",
[this, params, image]() { uploadPhotoCoro(params, image); }
);
}
void FSPrimfeedConnect::uploadPhotoCoro(const LLSD& params, LLImageFormatted* image)
{
LL_DEBUGS("primfeed") << "Entered uploadPhotoCoro" << LL_ENDL;
setConnectionState(PRIMFEED_POSTING);
LL_DEBUGS("primfeed") << "Connection state set to PRIMFEED_POSTING" << LL_ENDL;
const std::string fmt = (image->getCodec() == EImageCodec::IMG_CODEC_JPEG) ? "jpg" : "png";
LL_DEBUGS("primfeed") << "Image format: " << fmt << LL_ENDL;
const std::string boundary = "----------------------------0123456789abcdef";
const std::string sep = "\n";
const std::string dash = "--" + boundary;
LL_DEBUGS("primfeed") << "Building multipart body" << LL_ENDL;
LLCore::BufferArray::ptr_t raw(new LLCore::BufferArray());
LLCore::BufferArrayStream body(raw.get());
auto addPart = [&](const std::string& name, const std::string& val)
{
LL_DEBUGS("primfeed") << "Adding part: " << name << "=" << val << LL_ENDL;
body << dash << sep
<< "Content-Disposition: form-data; name=\"" << name << "\"" << sep << sep
<< val << sep;
};
addPart("commercial", params["commercial"].asBoolean() ? "true" : "false");
addPart("rating", params["rating"].asString());
addPart("content", params["content"].asString());
addPart("publicGallery", params["post_to_public_gallery"].asBoolean()? "true" : "false");
if (params.has("location") && !params["location"].asString().empty())
{
addPart("location", params["location"].asString());
}
LL_DEBUGS("primfeed") << "Adding image file header" << LL_ENDL;
body << dash << sep
<< "Content-Disposition: form-data; name=\"image\"; filename=\"snapshot." << fmt << "\"" << sep
<< "Content-Type: image/" << fmt << sep << sep;
U8* data = image->getData();
S32 size = image->getDataSize();
LL_DEBUGS("primfeed") << "Appending image data, size=" << size << LL_ENDL;
// yep this seems inefficient, but all other occurrences in the codebase do it this way.
for (S32 i = 0; i < size; ++i)
{
body << data[i];
}
body << sep;
body << dash << "--" << sep;
LL_DEBUGS("primfeed") << "Multipart body ready" << LL_ENDL;
// Setup HTTP
LL_DEBUGS("primfeed") << "Preparing HTTP request" << LL_ENDL;
LLCore::HttpRequest::policy_t policy = LLCore::HttpRequest::DEFAULT_POLICY_ID;
LLCoreHttpUtil::HttpCoroutineAdapter adapter("PrimfeedUpload", policy);
LLCore::HttpRequest::ptr_t request(new LLCore::HttpRequest);
LLCore::HttpOptions::ptr_t options(new LLCore::HttpOptions);
options->setWantHeaders(true);
LL_DEBUGS("primfeed") << "Setting HTTP headers" << LL_ENDL;
LLCore::HttpHeaders::ptr_t headers(new LLCore::HttpHeaders);
std::string token = gSavedPerAccountSettings.getString("FSPrimfeedOAuthToken");
std::string apiKey = gSavedSettings.getString("FSPrimfeedViewerApiKey");
headers->append("Authorization", "Bearer " + token);
headers->append("pf-viewer-api-key", apiKey);
headers->append("Content-Type", "multipart/form-data; boundary=" + boundary);
LL_DEBUGS("primfeed") << "Dumping HTTP headers for POST:" << LL_ENDL;
for (auto it = headers->begin(); it != headers->end(); ++it)
{
LL_DEBUGS("primfeed") << it->first << ": " << it->second << LL_ENDL;
}
LL_DEBUGS("primfeed") << "Headers set" << LL_ENDL;
LL_DEBUGS("primfeed") << "Starting HTTP POST" << LL_ENDL;
LLSD result = adapter.postRawAndSuspend(request,
"https://api.primfeed.com/pf/viewer/post",
raw,
options,
headers);
LL_DEBUGS("primfeed") << "HTTP POST complete" << LL_ENDL;
const LLSD::Binary &rawData = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary();
std::string response_raw;
response_raw.assign(rawData.begin(), rawData.end());
LLSD result_LLSD;
if(!response_raw.empty())
{
result_LLSD = LlsdFromJson(boost::json::parse(response_raw));
}
LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]);
bool success = (status.getType() == HTTP_OK);
LL_DEBUGS("primfeed") << "HTTP status =" << (success?"OK":"FAIL") << " "<< status.getMessage() << LL_ENDL;
std::string url;
if (success)
{
url = result_LLSD["url"].asString();
LL_DEBUGS("primfeed") << "Received URL=" << url << LL_ENDL;
}
LL_DEBUGS("primfeed") << "Invoking callback" << LL_ENDL;
mPostCallback(success, url);
setConnectionState(success ? PRIMFEED_POSTED : PRIMFEED_POST_FAILED);
LL_DEBUGS("primfeed") << "Final state set" << LL_ENDL;
}
// Handle connection state transitions
void FSPrimfeedConnect::setConnectionState(EConnectionState state)
{
LL_DEBUGS("primfeed") << "setConnectionState(" << state << ")" << LL_ENDL;
mConnectionState = state;
}
FSPrimfeedConnect::EConnectionState FSPrimfeedConnect::getConnectionState() const
{
return mConnectionState;
}
bool FSPrimfeedConnect::isTransactionOngoing() const
{
return (mConnectionState == PRIMFEED_CONNECTING ||
mConnectionState == PRIMFEED_POSTING ||
mConnectionState == PRIMFEED_DISCONNECTING);
}
void FSPrimfeedConnect::loadPrimfeedInfo()
{
LL_DEBUGS("primfeed") << "loadPrimfeedInfo() called" << LL_ENDL;
// Nothing to do here for Primfeed
setConnectionState(PRIMFEED_CONNECTED);
}