From c085682b546e18131ccad39fc256640a3062302c Mon Sep 17 00:00:00 2001 From: Beq Date: Sat, 8 Mar 2025 17:17:21 +0000 Subject: [PATCH] Primfeed integration V1 baseline Primfeed authentication/authorisation - working chat interception login authentication - working Primfeed Integration - WiP Authorisation connect/disconnect - working Primfeed post photo support - working Clean up ready for release --- indra/newview/CMakeLists.txt | 8 + indra/newview/app_settings/commands.xml | 10 + indra/newview/app_settings/settings.xml | 45 + .../app_settings/settings_per_account.xml | 99 ++ indra/newview/fsfloaterprimfeed.cpp | 910 ++++++++++++++++++ indra/newview/fsfloaterprimfeed.h | 155 +++ indra/newview/fsprimfeedauth.cpp | 468 +++++++++ indra/newview/fsprimfeedauth.h | 93 ++ indra/newview/fsprimfeedconnect.cpp | 189 ++++ indra/newview/fsprimfeedconnect.h | 82 ++ indra/newview/llappviewer.cpp | 2 + indra/newview/llfloatersnapshot.cpp | 17 +- indra/newview/llfloatersnapshot.h | 2 +- indra/newview/llpanelsnapshotoptions.cpp | 17 + indra/newview/llsnapshotlivepreview.cpp | 1 + indra/newview/llviewerfloaterreg.cpp | 2 + indra/newview/llviewermenu.cpp | 24 + indra/newview/pipeline.cpp | 12 +- .../skins/default/textures/icons/primfeed.png | Bin 0 -> 3246 bytes .../default/textures/icons/primfeed_white.png | Bin 0 -> 3226 bytes .../skins/default/textures/textures.xml | 1 + .../skins/default/xui/en/floater_primfeed.xml | 90 ++ .../skins/default/xui/en/menu_viewer.xml | 12 + .../skins/default/xui/en/notifications.xml | 69 ++ .../default/xui/en/panel_primfeed_account.xml | 105 ++ .../default/xui/en/panel_primfeed_photo.xml | 350 +++++++ .../default/xui/en/panel_snapshot_options.xml | 25 + .../newview/skins/default/xui/en/strings.xml | 6 + 28 files changed, 2784 insertions(+), 10 deletions(-) create mode 100644 indra/newview/fsfloaterprimfeed.cpp create mode 100644 indra/newview/fsfloaterprimfeed.h create mode 100644 indra/newview/fsprimfeedauth.cpp create mode 100644 indra/newview/fsprimfeedauth.h create mode 100644 indra/newview/fsprimfeedconnect.cpp create mode 100644 indra/newview/fsprimfeedconnect.h create mode 100644 indra/newview/skins/default/textures/icons/primfeed.png create mode 100644 indra/newview/skins/default/textures/icons/primfeed_white.png create mode 100644 indra/newview/skins/default/xui/en/floater_primfeed.xml create mode 100644 indra/newview/skins/default/xui/en/panel_primfeed_account.xml create mode 100644 indra/newview/skins/default/xui/en/panel_primfeed_photo.xml diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 8b0555fd0c..cbd633c56c 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -96,6 +96,7 @@ set(viewer_SOURCE_FILES dialogstack.cpp exoflickr.cpp exoflickrauth.cpp + fsprimfeedauth.cpp exogroupmutelist.cpp floatermedialists.cpp fsareasearch.cpp @@ -164,6 +165,7 @@ set(viewer_SOURCE_FILES fspose.cpp fsposeranimator.cpp fsposingmotion.cpp + fsprimfeedauth.cpp fsradar.cpp fsradarentry.cpp fsradarlistctrl.cpp @@ -182,6 +184,8 @@ set(viewer_SOURCE_FILES lfsimfeaturehandler.cpp llflickrconnect.cpp llfloaterflickr.cpp + fsprimfeedconnect.cpp + fsfloaterprimfeed.cpp llpanelopenregionsettings.cpp # [Legacy Bake] llagentwearablesfetch.cpp @@ -909,6 +913,7 @@ set(viewer_HEADER_FILES dialogstack.h exoflickr.h exoflickrauth.h + fsprimfeedauth.h exogroupmutelist.h floatermedialists.h fsareasearch.h @@ -979,6 +984,7 @@ set(viewer_HEADER_FILES fspose.h fsposeranimator.h fsposingmotion.h + fsprimfeedauth.h fsradar.h fsradarentry.h fsradarlistctrl.h @@ -998,6 +1004,8 @@ set(viewer_HEADER_FILES lfsimfeaturehandler.h llflickrconnect.h llfloaterflickr.h + fsprimfeedconnect.h + fsfloaterprimfeed.h # [Legacy Bake] llagentwearablesfetch.h vjlocalmesh.h diff --git a/indra/newview/app_settings/commands.xml b/indra/newview/app_settings/commands.xml index 7708f9a0c9..7f26b29024 100644 --- a/indra/newview/app_settings/commands.xml +++ b/indra/newview/app_settings/commands.xml @@ -254,6 +254,16 @@ is_running_function="Floater.IsOpen" is_running_parameters="flickr" /> + Value http://phoenixviewer.com/app/fsdata/grids.xml + FSPrimfeedViewerApiKey + + Comment + Viewer key for API login. + Persist + 1 + Type + String + Value + xAcXYt8SBius3Lor4wHle8L96PDHYlAZuWYXIYQUdW4b09mjhQUAwiqmWp5UNYXLpq5GSUtuKHuDYLwaueACPkew93l6MRY8jfBKSH09kv0zyGglpky07X7X7Sp4Rzin + FSGridBuilderURL Comment @@ -18542,6 +18553,7 @@ Change of this parameter will affect the layout of buttons in notification toast world_map preferences flickr + primfeed Backup 0 @@ -24729,6 +24741,39 @@ Change of this parameter will affect the layout of buttons in notification toast Value 4 + FSLastSnapshotToPrimfeedHeight + + Comment + The height of the last Primfeed snapshot, in px + Persist + 1 + Type + S32 + Value + 768 + + FSLastSnapshotToPrimfeedWidth + + Comment + The width of the last Primfeed snapshot, in px + Persist + 1 + Type + S32 + Value + 1024 + + FSLastSnapshotToPrimfeedResolution + + Comment + At what resolution should snapshots be posted on Primfeed. 0=Current Window, 1=320x240, 2=640x480, 3=800x600, 4=1024x768, 5=1280x1024, 6=1600x1200, 7=Custom + Persist + 1 + Type + S32 + Value + 4 + FSLastSnapshotToTwitterHeight Comment diff --git a/indra/newview/app_settings/settings_per_account.xml b/indra/newview/app_settings/settings_per_account.xml index 95e3c687dd..6c58ad7896 100644 --- a/indra/newview/app_settings/settings_per_account.xml +++ b/indra/newview/app_settings/settings_per_account.xml @@ -1358,6 +1358,94 @@ Value 0 + FSPrimfeedOAuthToken + + Comment + contains the secure authentication toke to post to your primfeed account (do not share) + Persist + 1 + Type + String + Value + + + FSPrimfeedProfileLink + + Comment + The profile page for the account associated with the currently linked Primfeed account + Persist + 1 + Type + String + Value + + + FSPrimfeedPlan + + Comment + The plan type associated with the currently linked Primfeed account + Persist + 1 + Type + String + Value + + + FSPrimfeedUsername + + Comment + The username associated with the currently linked Primfeed account + Persist + 1 + Type + String + Value + + + FSPrimfeedCommercialContent + + Comment + Does this post contain commercial content + Persist + 1 + Type + Boolean + Value + 0 + + FSPrimfeedAddToPublicGallery + + Comment + Should this post go to the public gallery? + Persist + 0 + Type + Boolean + Value + 0 + + FSPrimfeedAddToPublicGallery + + Comment + Should this post go to the public gallery? + Persist + 0 + Type + Boolean + Value + 0 + + FSPrimfeedOpenURLOnPost + + Comment + if true open the URL in a browser when the post completes + Persist + 1 + Type + Boolean + Value + 0 + FSProtectedFolders Comment @@ -1369,5 +1457,16 @@ Value + FSPrimfeedPhotoRating + + Comment + Content rating to be shared with Primfeed. + Persist + 1 + Type + Integer + Value + 1 + diff --git a/indra/newview/fsfloaterprimfeed.cpp b/indra/newview/fsfloaterprimfeed.cpp new file mode 100644 index 0000000000..28d558dbf4 --- /dev/null +++ b/indra/newview/fsfloaterprimfeed.cpp @@ -0,0 +1,910 @@ +/** +* @file fsfloaterprimfeed.cpp +* @brief Implementation of primfeed floater +* @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 "llviewerprecompiledheaders.h" + +#include "fsfloaterprimfeed.h" +#include "fsprimfeedconnect.h" +#include "llagent.h" +#include "llagentui.h" +#include "llcheckboxctrl.h" +#include "llcombobox.h" +#include "llfloaterreg.h" +#include "lliconctrl.h" +#include "llimagefiltersmanager.h" +#include "llresmgr.h" // LLLocale +#include "llsdserialize.h" +#include "llloadingindicator.h" +#include "llslurl.h" +#include "lltrans.h" +#include "llfloatersnapshot.h" +#include "llsnapshotlivepreview.h" +#include "llfloaterbigpreview.h" +#include "llviewerregion.h" +#include "llviewercontrol.h" +#include "llviewermedia.h" +#include "lltabcontainer.h" +#include "llviewerparcelmgr.h" +#include "llviewerregion.h" +#include +#include "llspinctrl.h" + +#include "llviewernetwork.h" +#include "llnotificationsutil.h" +#include "fsprimfeedauth.h" +#include "llviewernetwork.h" + +static LLPanelInjector t_panel_photo("fsprimfeedphotopanel"); +static LLPanelInjector t_panel_account("fsprimfeedaccountpanel"); + +/////////////////////////// +//FSPrimfeedPhotoPanel///// +/////////////////////////// + +FSPrimfeedPhotoPanel::FSPrimfeedPhotoPanel() : + mResolutionComboBox(nullptr), + mRefreshBtn(nullptr), + mBtnPreview(nullptr), + mWorkingLabel(nullptr), + mThumbnailPlaceholder(nullptr), + mDescriptionTextBox(nullptr), + mLocationCheckbox(nullptr), + mRatingComboBox(nullptr), + mBigPreviewFloater(nullptr), + mPostButton(nullptr) +{ + mCommitCallbackRegistrar.add("SocialSharing.SendPhoto", [this](LLUICtrl* ctrl, const LLSD& data) { onSend(); }); + mCommitCallbackRegistrar.add("SocialSharing.RefreshPhoto", [this](LLUICtrl* ctrl, const LLSD& data) { onClickNewSnapshot(); }); + mCommitCallbackRegistrar.add("SocialSharing.BigPreview", [this](LLUICtrl* ctrl, const LLSD& data) { onClickBigPreview(); }); + mCommitCallbackRegistrar.add("Primfeed.Info", [this](LLUICtrl* ctrl, const LLSD& param) { + const std::string url = param.asString(); + LL_DEBUGS("primfeed") << "Info button clicked, opening " << url << LL_ENDL; + LLWeb::loadURLExternal(url); + }); +} + +FSPrimfeedPhotoPanel::~FSPrimfeedPhotoPanel() +{ + if(mPreviewHandle.get()) + { + mPreviewHandle.get()->die(); + } + + FSPrimfeedAuth::sPrimfeedAuthPump->stopListening("FSPrimfeedAccountPanel"); + + gSavedSettings.setS32("FSLastSnapshotToPrimfeedResolution", getChild("resolution_combobox")->getCurrentIndex()); + gSavedSettings.setS32("FSLastSnapshotToPrimfeedWidth", getChild("custom_snapshot_width")->getValue().asInteger()); + gSavedSettings.setS32("FSLastSnapshotToPrimfeedHeight", getChild("custom_snapshot_height")->getValue().asInteger()); +} + +bool FSPrimfeedPhotoPanel::postBuild() +{ + setVisibleCallback([this](LLUICtrl * unused, bool visible) { + onVisibilityChange(visible); + }); + + mResolutionComboBox = getChild("resolution_combobox"); + mResolutionComboBox->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); }); + mFilterComboBox = getChild("filters_combobox"); + mFilterComboBox->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); }); + mRefreshBtn = getChild("new_snapshot_btn"); + mBtnPreview = getChild("big_preview_btn"); + mWorkingLabel = getChild("working_lbl"); + mThumbnailPlaceholder = getChild("thumbnail_placeholder"); + mDescriptionTextBox = getChild("photo_description"); + mLocationCheckbox = getChild("add_location_cb"); + mCommercialCheckbox = getChild("primfeed_commercial_content"); + mPublicGalleryCheckbox = getChild("primfeed_add_to_public_gallery"); + mRatingComboBox = getChild("rating_combobox"); + mPostButton = getChild("post_photo_btn"); + mCancelButton = getChild("cancel_photo_btn"); + mBigPreviewFloater = dynamic_cast(LLFloaterReg::getInstance("big_preview")); + + // Update custom resolution controls with lambdas + getChild("custom_snapshot_width")->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); }); + getChild("custom_snapshot_height")->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); }); + getChild("keep_aspect_ratio")->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); }); + + getChild("resolution_combobox")->setCurrentByIndex(gSavedSettings.getS32("FSLastSnapshotToPrimfeedResolution")); + getChild("custom_snapshot_width")->setValue(gSavedSettings.getS32("FSLastSnapshotToPrimfeedWidth")); + getChild("custom_snapshot_height")->setValue(gSavedSettings.getS32("FSLastSnapshotToPrimfeedHeight")); + + // Update filter list + std::vector filter_list = LLImageFiltersManager::getInstance()->getFiltersList(); + LLComboBox* filterbox = static_cast(mFilterComboBox); + for (U32 i = 0; i < filter_list.size(); i++) + { + filterbox->add(filter_list[i]); + } + + return LLPanel::postBuild(); +} + +//static +void FSFloaterPrimfeed::update() +{ + if (LLFloaterReg::instanceVisible("primfeed")) + { + LLFloaterSnapshotBase::ImplBase::updatePreviewList( true, true ); + } +} + +// virtual +S32 FSPrimfeedPhotoPanel::notify(const LLSD& info) +{ + if (info.has("snapshot-updating")) + { + // Disable the Post button and whatever else while the snapshot is not updated + // updateControls(); + return 1; + } + + if (info.has("snapshot-updated")) + { + // Enable the send/post/save buttons. + updateControls(); + + // The refresh button is initially hidden. We show it after the first update, + // i.e. after snapshot is taken + LLUICtrl * refresh_button = getRefreshBtn(); + if (!refresh_button->getVisible()) + { + refresh_button->setVisible(true); + } + return 1; + } + + return 0; +} + +void FSPrimfeedPhotoPanel::draw() +{ + LLSnapshotLivePreview * previewp = static_cast(mPreviewHandle.get()); + + // Enable interaction only if no transaction with the service is on-going (prevent duplicated posts) + auto can_post = !(FSPrimfeedConnect::instance().isTransactionOngoing()) && FSPrimfeedAuth::isAuthorized(); + + mCancelButton->setEnabled(can_post); + mDescriptionTextBox->setEnabled(can_post); + mRatingComboBox->setEnabled(can_post); + mResolutionComboBox->setEnabled(can_post); + mFilterComboBox->setEnabled(can_post); + mRefreshBtn->setEnabled(can_post); + mBtnPreview->setEnabled(can_post); + mLocationCheckbox->setEnabled(can_post); + + // Reassign the preview floater if we have the focus and the preview exists + if (hasFocus() && isPreviewVisible()) + { + attachPreview(); + } + + // Toggle the button state as appropriate + bool preview_active = (isPreviewVisible() && mBigPreviewFloater->isFloaterOwner(getParentByType())); + mBtnPreview->setToggleState(preview_active); + + // Display the preview if one is available + if (previewp && previewp->getThumbnailImage()) + { + const LLRect& thumbnail_rect = mThumbnailPlaceholder->getRect(); + const S32 thumbnail_w = previewp->getThumbnailWidth(); + const S32 thumbnail_h = previewp->getThumbnailHeight(); + + // calc preview offset within the preview rect + const S32 local_offset_x = (thumbnail_rect.getWidth() - thumbnail_w) / 2 ; + const S32 local_offset_y = (thumbnail_rect.getHeight() - thumbnail_h) / 2 ; + S32 offset_x = thumbnail_rect.mLeft + local_offset_x; + S32 offset_y = thumbnail_rect.mBottom + local_offset_y; + + gGL.matrixMode(LLRender::MM_MODELVIEW); + // Apply floater transparency to the texture unless the floater is focused. + F32 alpha = getTransparencyType() == TT_ACTIVE ? 1.0f : getCurrentTransparency(); + LLColor4 color = LLColor4::white; + gl_draw_scaled_image(offset_x, offset_y, + thumbnail_w, thumbnail_h, + previewp->getThumbnailImage(), color % alpha); + } + + // Update the visibility of the working (computing preview) label + mWorkingLabel->setVisible(!(previewp && previewp->getSnapshotUpToDate())); + + // Enable Post if we have a preview to send and no on going connection being processed + mPostButton->setEnabled(can_post && (previewp && previewp->getSnapshotUpToDate())); + + // Draw the rest of the panel on top of it + LLPanel::draw(); +} + +LLSnapshotLivePreview* FSPrimfeedPhotoPanel::getPreviewView() +{ + LLSnapshotLivePreview* previewp = (LLSnapshotLivePreview*)mPreviewHandle.get(); + return previewp; +} + +void FSPrimfeedPhotoPanel::onVisibilityChange(bool visible) +{ + if (visible) + { + if (mPreviewHandle.get()) + { + LLSnapshotLivePreview* preview = getPreviewView(); + if(preview) + { + LL_DEBUGS() << "opened, updating snapshot" << LL_ENDL; + preview->updateSnapshot(true); + } + } + else + { + LLRect full_screen_rect = getRootView()->getRect(); + LLSnapshotLivePreview::Params p; + p.rect(full_screen_rect); + LLSnapshotLivePreview* previewp = new LLSnapshotLivePreview(p); + mPreviewHandle = previewp->getHandle(); + + previewp->setContainer(this); + previewp->setSnapshotType(LLSnapshotModel::SNAPSHOT_WEB); + previewp->setSnapshotFormat(LLSnapshotModel::SNAPSHOT_FORMAT_PNG); + previewp->setThumbnailSubsampled(true); // We want the preview to reflect the *saved* image + previewp->setAllowRenderUI(false); // We do not want the rendered UI in our snapshots + previewp->setAllowFullScreenPreview(false); // No full screen preview in SL Share mode + previewp->setThumbnailPlaceholderRect(mThumbnailPlaceholder->getRect()); + + updateControls(); + } + } +} + +void FSPrimfeedPhotoPanel::onClickNewSnapshot() +{ + LLSnapshotLivePreview* previewp = getPreviewView(); + if (previewp) + { + previewp->updateSnapshot(true); + } +} + +void FSPrimfeedPhotoPanel::onClickBigPreview() +{ + // Toggle the preview + if (isPreviewVisible()) + { + LLFloaterReg::hideInstance("big_preview"); + } + else + { + attachPreview(); + LLFloaterReg::showInstance("big_preview"); + } +} + +bool FSPrimfeedPhotoPanel::isPreviewVisible() +{ + return (mBigPreviewFloater && mBigPreviewFloater->getVisible()); +} + +void FSPrimfeedPhotoPanel::attachPreview() +{ + if (mBigPreviewFloater) + { + LLSnapshotLivePreview* previewp = getPreviewView(); + mBigPreviewFloater->setPreview(previewp); + mBigPreviewFloater->setFloaterOwner(getParentByType()); + } +} + +void FSPrimfeedPhotoPanel::onSend() +{ + sendPhoto(); +} + +bool FSPrimfeedPhotoPanel::onPrimfeedConnectStateChange(const LLSD& data) +{ + if (FSPrimfeedAuth::isAuthorized()) + { + sendPhoto(); + } + + return false; +} + +void FSPrimfeedPhotoPanel::sendPhoto() +{ + static const std::array RATING_NAMES = { + "general", // 1 + "moderate", // 2 + "adult", // 3 + "adult_plus" // 4 + }; + + auto ratingToString = [&](int rating) -> std::string { + // clamp into [1,4] + int idx = llclamp(rating, 1, 4) - 1; + return RATING_NAMES[idx]; + }; + // Get the description (primfeed has no title/tags etc at this point) + std::string description = mDescriptionTextBox->getValue().asString(); + + + // Get the content rating + int content_rating = mRatingComboBox->getValue().asInteger(); + bool post_to_public_gallery = mPublicGalleryCheckbox->getValue().asBoolean(); + bool commercial_content = mCommercialCheckbox->getValue().asBoolean(); + + // Get the image + LLSnapshotLivePreview* previewp = getPreviewView(); + + FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_POSTING); + LLSD params; + params["rating"] = ratingToString(content_rating); + params["content"] = description; + params["is_commercial"] = commercial_content; + params["post_to_public_gallery"] = post_to_public_gallery; + // Add the location if required + bool add_location = mLocationCheckbox->getValue().asBoolean(); + if (add_location) + { + // Get the SLURL for the location + LLSLURL slurl; + LLAgentUI::buildSLURL(slurl); + std::string slurl_string = slurl.getSLURLString(); + + params["location"] = slurl_string; + } + + FSPrimfeedConnect::instance().uploadPhoto(params, previewp->getFormattedImage().get(), + [this](bool success, const std::string& url) + { + if (success) + { + FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_POSTED); + static LLCachedControl open_url_on_post(gSavedPerAccountSettings, "FSPrimfeedOpenURLOnPost", true); + if (open_url_on_post) + { + LLWeb::loadURLExternal(url); + } + LLSD args; + args["PF_POSTURL"] = url; + LLNotificationsUtil::add("FSPrimfeedUploadComplete", args); + } + else + { + mWorkingLabel->setValue("Error posting to Primfeed"); + mPostButton->setEnabled(true); + } + } + ); + updateControls(); +} + +void FSPrimfeedPhotoPanel::clearAndClose() +{ + mDescriptionTextBox->setValue(""); + + LLFloater* floater = getParentByType(); + if (floater) + { + floater->closeFloater(); + if (mBigPreviewFloater) + { + mBigPreviewFloater->closeOnFloaterOwnerClosing(floater); + } + } +} + +void FSPrimfeedPhotoPanel::updateControls() +{ + LLSnapshotLivePreview* previewp = getPreviewView(); + bool got_snap = previewp && previewp->getSnapshotUpToDate(); + + updateResolution(false); +} + +void FSPrimfeedPhotoPanel::updateResolution(bool do_update) +{ + LLComboBox* combobox = static_cast(mResolutionComboBox); + LLComboBox* filterbox = static_cast(mFilterComboBox); + + std::string sdstring = combobox->getSelectedValue(); + LLSD sdres; + std::stringstream sstream(sdstring); + LLSDSerialize::fromNotation(sdres, sstream, sdstring.size()); + + S32 width = sdres[0]; + S32 height = sdres[1]; + + // Note : index 0 of the filter drop down is assumed to be "No filter" in whichever locale + std::string filter_name = (filterbox->getCurrentIndex() ? filterbox->getSimple() : ""); + + LLSnapshotLivePreview * previewp = static_cast(mPreviewHandle.get()); + if (previewp && combobox->getCurrentIndex() >= 0) + { + checkAspectRatio(width); + + S32 original_width = 0 , original_height = 0 ; + previewp->getSize(original_width, original_height) ; + + if (width == 0 || height == 0) + { + // take resolution from current window size + LL_DEBUGS() << "Setting preview res from window: " << gViewerWindow->getWindowWidthRaw() << "x" << gViewerWindow->getWindowHeightRaw() << LL_ENDL; + previewp->setSize(gViewerWindow->getWindowWidthRaw(), gViewerWindow->getWindowHeightRaw()); + } + else if (width == -1 || height == -1) + { + // take resolution from custom size + LLSpinCtrl* width_spinner = getChild("custom_snapshot_width"); + LLSpinCtrl* height_spinner = getChild("custom_snapshot_height"); + S32 custom_width = width_spinner->getValue().asInteger(); + S32 custom_height = height_spinner->getValue().asInteger(); + if (checkImageSize(previewp, custom_width, custom_height, true, previewp->getMaxImageSize())) + { + width_spinner->set((F32)custom_width); + height_spinner->set((F32)custom_height); + } + LL_DEBUGS() << "Setting preview res from custom: " << custom_width << "x" << custom_height << LL_ENDL; + previewp->setSize(custom_width, custom_height); + } + else + { + // use the resolution from the selected pre-canned drop-down choice + LL_DEBUGS() << "Setting preview res selected from combo: " << width << "x" << height << LL_ENDL; + previewp->setSize(width, height); + } + + + previewp->getSize(width, height); + if ((original_width != width) || (original_height != height)) + { + previewp->setSize(width, height); + if (do_update) + { + previewp->updateSnapshot(true, true); + updateControls(); + } + } + // Get the old filter, compare to the current one "filter_name" and set if changed + std::string original_filter = previewp->getFilter(); + if (original_filter != filter_name) + { + previewp->setFilter(filter_name); + if (do_update) + { + previewp->updateSnapshot(false, true); + updateControls(); + } + } + } + + bool custom_resolution = static_cast(mResolutionComboBox)->getSelectedValue().asString() == "[i-1,i-1]"; + getChild("custom_snapshot_width")->setEnabled(custom_resolution); + getChild("custom_snapshot_height")->setEnabled(custom_resolution); + getChild("keep_aspect_ratio")->setEnabled(custom_resolution); +} + +void FSPrimfeedPhotoPanel::checkAspectRatio(S32 index) +{ + LLSnapshotLivePreview *previewp = getPreviewView() ; + + bool keep_aspect = false; + + if (0 == index) // current window size + { + keep_aspect = true; + } + else if (-1 == index) + { + keep_aspect = getChild("keep_aspect_ratio")->get(); + } + else // predefined resolution + { + keep_aspect = false; + } + + if (previewp) + { + previewp->mKeepAspectRatio = keep_aspect; + } +} + +LLUICtrl* FSPrimfeedPhotoPanel::getRefreshBtn() +{ + return mRefreshBtn; +} + +void FSPrimfeedPhotoPanel::onOpen(const LLSD& key) +{ + if (!FSPrimfeedAuth::isAuthorized()) + { + // Reauthorise if necessary. + FSPrimfeedAuth::initiateAuthRequest(); + } +} + +void FSPrimfeedPhotoPanel::uploadCallback(bool success, const LLSD& response) +{ + LLSD args; + if(success && response["stat"].asString() == "ok") + { + FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_POSTED); + args["PF_POSTURL"] = response["postUrl"]; + LLNotificationsUtil::add("FSPrimfeedUploadComplete", args); + } + else + { + FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_POST_FAILED); + } +} + +void FSPrimfeedPhotoPanel::primfeedAuthResponse(bool success, const LLSD& response) +{ + if(!success) + { + if(response.has("status") && response["status"].asString() == "reset") + { + LL_INFOS("Primfeed") << "Primfeed authorization has been reset." << LL_ENDL; + } + else + { + // Complain about failed auth here. + LL_WARNS("Primfeed") << "Primfeed authentication failed." << LL_ENDL; + } + } + onPrimfeedConnectStateChange(response); +} + +bool FSPrimfeedPhotoPanel::checkImageSize(LLSnapshotLivePreview* previewp, S32& width, S32& height, bool isWidthChanged, S32 max_value) +{ + S32 w = width ; + S32 h = height ; + + if(previewp && previewp->mKeepAspectRatio) + { + if(gViewerWindow->getWindowWidthRaw() < 1 || gViewerWindow->getWindowHeightRaw() < 1) + { + return false; + } + + //aspect ratio of the current window + F32 aspect_ratio = (F32)gViewerWindow->getWindowWidthRaw() / gViewerWindow->getWindowHeightRaw() ; + + //change another value proportionally + if(isWidthChanged) + { + height = ll_round(width / aspect_ratio) ; + } + else + { + width = ll_round(height * aspect_ratio) ; + } + + //bound w/h by the max_value + if(width > max_value || height > max_value) + { + if(width > height) + { + width = max_value ; + height = (S32)(width / aspect_ratio) ; + } + else + { + height = max_value ; + width = (S32)(height * aspect_ratio) ; + } + } + } + + return (w != width || h != height) ; +} + +/////////////////////////// +//FSPrimfeedAccountPanel/// +/////////////////////////// + +FSPrimfeedAccountPanel::FSPrimfeedAccountPanel() : +mAccountConnectedAsLabel(nullptr), +mAccountNameLink(nullptr), +mAccountPlan(nullptr), +mPanelButtons(nullptr), +mConnectButton(nullptr), +mDisconnectButton(nullptr) +{ + mCommitCallbackRegistrar.add("SocialSharing.Connect", [this](LLUICtrl* ctrl, const LLSD& data) { onConnect(); }); + mCommitCallbackRegistrar.add("SocialSharing.Disconnect", [this](LLUICtrl* ctrl, const LLSD& data) { onDisconnect(); }); + + FSPrimfeedAuth::sPrimfeedAuthPump->listen("FSPrimfeedAccountPanel", + [this](const LLSD& data) -> bool + { + bool success = data["success"].asBoolean(); + this->primfeedAuthResponse(success, data); + return true; + } + ); + + setVisibleCallback([this](LLUICtrl *unused, bool visible) { + onVisibilityChange(visible); + }); +} + +bool FSPrimfeedAccountPanel::postBuild() +{ + mAccountConnectedAsLabel= getChild("connected_as_label"); + mAccountNameLink = getChild("primfeed_account_name"); + mAccountPlan = getChild("primfeed_account_plan"); + mPanelButtons = getChild("panel_buttons"); + mConnectButton = getChild("connect_btn"); + mDisconnectButton = getChild("disconnect_btn"); + + LLSD dummy; + onPrimfeedConnectStateChange(dummy); + return LLPanel::postBuild(); +} + +void FSPrimfeedAccountPanel::draw() +{ + FSPrimfeedConnect::EConnectionState connection_state = FSPrimfeedConnect::instance().getConnectionState(); + + //Disable the 'disconnect' button and the 'use another account' button when disconnecting in progress + bool disconnecting = connection_state == FSPrimfeedConnect::PRIMFEED_DISCONNECTING; + mDisconnectButton->setEnabled(!disconnecting); + + //Disable the 'connect' button when a connection is in progress + bool connecting = ( connection_state == FSPrimfeedConnect::PRIMFEED_CONNECTING || + connection_state == FSPrimfeedConnect::PRIMFEED_CONNECTED ); + mConnectButton->setEnabled(!connecting); + + LLPanel::draw(); +} + +void FSPrimfeedAccountPanel::primfeedAuthResponse(bool success, const LLSD& response) +{ + if(!success) + { + LL_WARNS("Primfeed") << "Primfeed authentication failed." << LL_ENDL; + LLWeb::loadURLExternal("https://www.primfeed.com/login"); + } + onPrimfeedConnectStateChange(response); +} + +void FSPrimfeedAccountPanel::onVisibilityChange(bool visible) +{ + if(visible) + { + + + //Connected + if(FSPrimfeedAuth::isAuthorized()) + { + showConnectedLayout(); + } + else + { + showDisconnectedLayout(); + } + } +} + +bool FSPrimfeedAccountPanel::onPrimfeedConnectStateChange(const LLSD& data) +{ + if (FSPrimfeedAuth::isAuthorized()) + { + showConnectedLayout(); + } + else + { + showDisconnectedLayout(); + } + onPrimfeedConnectInfoChange(); + return false; +} + +bool FSPrimfeedAccountPanel::onPrimfeedConnectInfoChange() +{ + std::string clickable_name{""}; + + static LLCachedControl primfeed_username(gSavedPerAccountSettings, "FSPrimfeedUsername"); + static LLCachedControl primfeed_profile_link(gSavedPerAccountSettings, "FSPrimfeedProfileLink"); + static LLCachedControl primfeed_plan(gSavedPerAccountSettings, "FSPrimfeedPlan"); + + //Strings of format [http://www.somewebsite.com Click Me] become clickable text + if (!primfeed_username().empty()) + { + clickable_name = std::string("[") + std::string(primfeed_profile_link) + " " + std::string(primfeed_username) + "]"; + } + + mAccountNameLink->setText(clickable_name); + mAccountPlan->setText(primfeed_plan()); + + return false; +} + +void FSPrimfeedAccountPanel::showConnectButton() +{ + if(!mConnectButton->getVisible()) + { + mConnectButton->setVisible(true); + mDisconnectButton->setVisible(false); + } +} + +void FSPrimfeedAccountPanel::hideConnectButton() +{ + if(mConnectButton->getVisible()) + { + mConnectButton->setVisible(false); + mDisconnectButton->setVisible(true); + } +} + +void FSPrimfeedAccountPanel::showDisconnectedLayout() +{ + mAccountConnectedAsLabel->setText(getString("primfeed_disconnected")); + mAccountNameLink->setText(std::string("")); + mAccountPlan->setText(getString("primfeed_plan_unknown")); + showConnectButton(); +} + +void FSPrimfeedAccountPanel::showConnectedLayout() +{ + mAccountConnectedAsLabel->setText(getString("primfeed_connected")); + hideConnectButton(); +} + +void FSPrimfeedAccountPanel::onConnect() +{ + FSPrimfeedAuth::initiateAuthRequest(); +} + +void FSPrimfeedAccountPanel::onDisconnect() +{ + FSPrimfeedAuth::resetAuthStatus(); +} + +//////////////////////// +//FSFloaterPrimfeed///// +//////////////////////// + +FSFloaterPrimfeed::FSFloaterPrimfeed(const LLSD& key) : LLFloater(key), + mPrimfeedPhotoPanel(nullptr), + mStatusErrorText(nullptr), + mStatusLoadingText(nullptr), + mStatusLoadingIndicator(nullptr) +{ + mCommitCallbackRegistrar.add("SocialSharing.Cancel", [this](LLUICtrl* ctrl, const LLSD& data) { onCancel(); }); +} + +void FSFloaterPrimfeed::onClose(bool app_quitting) +{ + LLFloaterBigPreview* big_preview_floater = dynamic_cast(LLFloaterReg::getInstance("big_preview")); + if (big_preview_floater) + { + big_preview_floater->closeOnFloaterOwnerClosing(this); + } + LLFloater::onClose(app_quitting); +} + +void FSFloaterPrimfeed::onCancel() +{ + LLFloaterBigPreview* big_preview_floater = dynamic_cast(LLFloaterReg::getInstance("big_preview")); + if (big_preview_floater) + { + big_preview_floater->closeOnFloaterOwnerClosing(this); + } + closeFloater(); +} + +bool FSFloaterPrimfeed::postBuild() +{ + // Keep tab of the Photo Panel + mPrimfeedPhotoPanel = static_cast(getChild("panel_primfeed_photo")); + mPrimfeedAccountPanel = static_cast(getChild("panel_primfeed_account")); + // Connection status widgets + mStatusErrorText = getChild("connection_error_text"); + mStatusLoadingText = getChild("connection_loading_text"); + mStatusLoadingIndicator = getChild("connection_loading_indicator"); + + return LLFloater::postBuild(); +} + +void FSFloaterPrimfeed::showPhotoPanel() +{ + LLTabContainer* parent = dynamic_cast(mPrimfeedPhotoPanel->getParent()); + if (!parent) + { + LL_WARNS() << "Cannot find panel container" << LL_ENDL; + return; + } + + parent->selectTabPanel(mPrimfeedPhotoPanel); +} + +void FSFloaterPrimfeed::draw() +{ + if (mStatusErrorText && mStatusLoadingText && mStatusLoadingIndicator) + { + mStatusErrorText->setVisible(false); + mStatusLoadingText->setVisible(false); + mStatusLoadingIndicator->setVisible(false); + + FSPrimfeedConnect::EConnectionState connection_state = FSPrimfeedConnect::instance().getConnectionState(); + std::string status_text; + + if (FSPrimfeedAuth::isAuthorized()) + { + switch (connection_state) + { + case FSPrimfeedConnect::PRIMFEED_POSTING: + { + // Posting indicator + mStatusLoadingText->setVisible(true); + status_text = LLTrans::getString("SocialPrimfeedPosting"); + mStatusLoadingText->setValue(status_text); + mStatusLoadingIndicator->setVisible(true); + break; + } + case FSPrimfeedConnect::PRIMFEED_POST_FAILED: + { + // Error posting to the service + mStatusErrorText->setVisible(true); + status_text = LLTrans::getString("SocialPrimfeedErrorPosting"); + mStatusErrorText->setValue(status_text); + break; + } + } + } + else if (FSPrimfeedAuth::isPendingAuth()) + { + // Show the status text when authorisation is pending + mStatusLoadingText->setVisible(true); + status_text = LLTrans::getString("SocialPrimfeedConnecting"); + mStatusLoadingText->setValue(status_text); + } + else + { + // Show the status text when not authorised + mStatusErrorText->setVisible(true); + status_text = LLTrans::getString("SocialPrimfeedNotAuthorized"); + mStatusErrorText->setValue(status_text); + } + } + LLFloater::draw(); +} + + +void FSFloaterPrimfeed::onOpen(const LLSD& key) +{ + mPrimfeedPhotoPanel->onOpen(key); +} +LLSnapshotLivePreview* FSFloaterPrimfeed::getPreviewView() +{ + if(mPrimfeedPhotoPanel) + { + return mPrimfeedPhotoPanel->getPreviewView(); + } + return nullptr; +} diff --git a/indra/newview/fsfloaterprimfeed.h b/indra/newview/fsfloaterprimfeed.h new file mode 100644 index 0000000000..58c864577b --- /dev/null +++ b/indra/newview/fsfloaterprimfeed.h @@ -0,0 +1,155 @@ +/** +* @file fsfloaterprimfeed.cpp +* @brief Declaration of primfeed floater +* @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$ +*/ +#ifndef FS_FLOATERPRIMFEED_H +#define FS_FLOATERPRIMFEED_H + +#include "llfloater.h" +#include "lltextbox.h" +#include "llviewertexture.h" + +class LLIconCtrl; +class LLCheckBoxCtrl; +class LLSnapshotLivePreview; +class LLFloaterBigPreview; + +/* + * (TODO) Beq: Refactor this with Flickr + * Primfeed floater is copied heavily from the LLFlaoterFlickr class and deliberately implemetns much of the underlying plumbinng into the connector class. + * Once this is bedded in and any initial issues are addressed, it would be sensible to refactor both the flickr and primfeed classes to share a common base. + * In particular a ref counted test for the livepreview would eliminate the need for the static update method in the app mainloop. +*/ +class FSPrimfeedPhotoPanel : public LLPanel +{ +public: + FSPrimfeedPhotoPanel(); + ~FSPrimfeedPhotoPanel(); + + bool postBuild(); + S32 notify(const LLSD& info); + void draw(); + + LLSnapshotLivePreview* getPreviewView(); + void onVisibilityChange(bool new_visibility); + void onClickNewSnapshot(); + void onClickBigPreview(); + void onSend(); + bool onPrimfeedConnectStateChange(const LLSD& data); + + void sendPhoto(); + void clearAndClose(); + + void updateControls(); + void updateResolution(bool do_update); + void checkAspectRatio(S32 index); + LLUICtrl* getRefreshBtn(); + + /*virtual*/ void onOpen(const LLSD& key); + void primfeedAuthResponse(bool success, const LLSD& response); + void uploadCallback(bool success, const LLSD& response); + +private: + bool isPreviewVisible(); + void attachPreview(); + + bool checkImageSize(LLSnapshotLivePreview* previewp, S32& width, S32& height, bool isWidthChanged, S32 max_value); + + LLHandle mPreviewHandle; + + LLUICtrl * mResolutionComboBox; + LLUICtrl * mFilterComboBox; + LLUICtrl * mRefreshBtn; + LLUICtrl * mWorkingLabel; + LLUICtrl * mThumbnailPlaceholder; + LLUICtrl * mDescriptionTextBox; + LLUICtrl * mLocationCheckbox; + + LLUICtrl * mCommercialCheckbox; + LLUICtrl * mPublicGalleryCheckbox; + LLUICtrl * mRatingComboBox; + LLUICtrl * mPostButton; + LLUICtrl * mCancelButton; + LLButton * mBtnPreview; + + LLFloaterBigPreview * mBigPreviewFloater; +}; + +class FSPrimfeedAccountPanel : public LLPanel +{ +public: + FSPrimfeedAccountPanel(); + bool postBuild(); + void draw(); + +private: + void onVisibilityChange(bool new_visibility); + void primfeedAuthResponse(bool success, const LLSD& response); + bool onPrimfeedConnectStateChange(const LLSD& data); + bool onPrimfeedConnectInfoChange(); + void onConnect(); + void onUseAnotherAccount(); + void onDisconnect(); + + void showConnectButton(); + void hideConnectButton(); + void showDisconnectedLayout(); + void showConnectedLayout(); + + LLTextBox * mAccountConnectedAsLabel; + LLTextBox * mAccountNameLink; + LLTextBox * mAccountPlan; + LLUICtrl * mPanelButtons; + LLUICtrl * mConnectButton; + LLUICtrl * mDisconnectButton; +}; + + +class FSFloaterPrimfeed : public LLFloater +{ +public: + FSFloaterPrimfeed(const LLSD& key); + static void update(); + bool postBuild(); + void draw(); + void onClose(bool app_quitting); + void onCancel(); + + void showPhotoPanel(); + + void onOpen(const LLSD& key); + LLSnapshotLivePreview* getPreviewView(); + +private: + FSPrimfeedPhotoPanel* mPrimfeedPhotoPanel; + FSPrimfeedAccountPanel* mPrimfeedAccountPanel; + LLTextBox* mStatusErrorText; + LLTextBox* mStatusLoadingText; + LLUICtrl* mStatusLoadingIndicator; +}; + +#endif // LL_FSFLOATERPRIMFEED_H + diff --git a/indra/newview/fsprimfeedauth.cpp b/indra/newview/fsprimfeedauth.cpp new file mode 100644 index 0000000000..2321474ec5 --- /dev/null +++ b/indra/newview/fsprimfeedauth.cpp @@ -0,0 +1,468 @@ +/** + * @file fsprimfeedauth.cpp + * @file fsprimfeedauth.h + * @brief Primfeed Authorisation workflow 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$ +* +/* + * Handles Primfeed authentication and authorisation through a multi-factor OAuth flow. + * + * This module integrates with Primfeed’s Third Party Viewers API. + * The authentication flow is as follows: + * 1. Initiate a login request: + * POST https://api.primfeed.com/pf/viewer/create-login-request + * Headers: + * pf-viewer-api-key: + * pf-user-uuid: + * Response: + * { "requestId": "<64-char string>" } + * + * 2. Redirect the user to: + * https://www.primfeed.com/oauth/viewer?r=&v= + * + * 3. The user is shown an approval screen. When they click Authorize, + * an in-world message is sent: + * #PRIMFEED_OAUTH: + * We intercept this code through an onChat handle then call onOauthTokenReceived(). + * + * 4. Validate the login request: + * POST https://api.primfeed.com/pf/viewer/validate-request + * Headers: + * Authorization: Bearer + * pf-viewer-api-key: + * pf-viewer-request-id: + * Response: HTTP 204 + * + * 5. Optionally, check user status: + * GET https://api.primfeed.com/pf/viewer/user + * Headers: + * Authorization: Bearer + * pf-viewer-api-key: + * Response: { "plan": "free" } (or "pro") + */ +#include "llviewerprecompiledheaders.h" +#include "fsprimfeedauth.h" +#include "fsprimfeedconnect.h" +#include "llimview.h" +#include "llnotificationsutil.h" +#include "llfloaterimnearbychathandler.h" +#include "llnotificationmanager.h" +#include "llagent.h" +#include "llevents.h" +#include "fscorehttputil.h" +#include "llwindow.h" +#include "llviewerwindow.h" +#include "lluri.h" +#include "llsdjson.h" +#include + +using Callback = FSPrimfeedAuth::authorized_callback_t; + +// private instance variable +std::shared_ptr FSPrimfeedAuth::sPrimfeedAuth; +std::unique_ptr FSPrimfeedAuth::sPrimfeedAuthPump = std::make_unique("PrimfeedAuthResponse"); + +// Helper callback that unpacks HTTP POST response data. +void FSPrimfeedAuthResponse(LLSD const &aData, Callback callback) +{ + LLSD header = aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS][LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_HEADERS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD( + aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]); + + const LLSD::Binary &rawData = aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary(); + std::string result; + result.assign(rawData.begin(), rawData.end()); + + // Assume JSON response. + + LLSD resultLLSD; + if(!result.empty()) + { + resultLLSD = LlsdFromJson(boost::json::parse(result)); + } + callback((status.getType() == HTTP_OK || + status.getType() == HTTP_NO_CONTENT), resultLLSD); +} + +void FSPrimfeedAuth::initiateAuthRequest() +{ + // This function is called to initiate the authentication request. + // It should be called when the user clicks the "Authenticate" button. + // Also triggered on opening the floater. + // The actual implementation is in the create() method. + + if (!isAuthorized()) + { + if (sPrimfeedAuth) + { + LLNotificationsUtil::add("PrimfeedAuthorisationAlreadyInProgress"); + return; + } + // If no token stored, begin the login request; otherwise check user status. + sPrimfeedAuth = FSPrimfeedAuth::create( + [](bool success, const LLSD &response) + { + LLSD event_data = response; + event_data["success"] = success; + sPrimfeedAuthPump->post(event_data); + // Now that auth is complete, clear the static pointer. + sPrimfeedAuth.reset(); + } + ); + } + else + { + LLNotificationsUtil::add("PrimfeedAlreadyAuthorized"); + } +} + +void FSPrimfeedAuth::resetAuthStatus() +{ + sPrimfeedAuth.reset(); + gSavedPerAccountSettings.setString("FSPrimfeedOAuthToken", ""); + gSavedPerAccountSettings.setString("FSPrimfeedProfileLink", ""); + gSavedPerAccountSettings.setString("FSPrimfeedPlan", ""); + gSavedPerAccountSettings.setString("FSPrimfeedUsername", ""); + LLSD event_data; + event_data["status"] = "reset"; + event_data["success"] = "false"; + sPrimfeedAuthPump->post(event_data); +} + + +FSPrimfeedAuth::FSPrimfeedAuth(authorized_callback_t callback) + : mCallback(callback), mAuthenticating(false) +{ + mInstantMessageConnection = LLIMModel::instance().addNewMsgCallback( + [this](const LLSD &message) { + LL_DEBUGS("FSPrimfeedAuth") << "Received chat message: " << message["message"].asString() << LL_ENDL; + this->onChatMessage(message); + }); + mChatMessageConnection = LLNotificationsUI::LLNotificationManager::instance().getChatHandler()->addNewChatCallback( + [this](const LLSD &message) { + LL_DEBUGS("FSPrimfeedAuth") << "Received instant message: " << message["message"].asString() << LL_ENDL; + this->onChatMessage(message); + }); +} + +FSPrimfeedAuth::~FSPrimfeedAuth() +{ + if (mChatMessageConnection.connected()) + { + try + { + mChatMessageConnection.disconnect(); + } + catch (const std::exception& e) + { + LL_WARNS("FSPrimfeedAuth") << "Exception during chat connection disconnect: " << e.what() << LL_ENDL; + } + catch (...) + { + LL_WARNS("FSPrimfeedAuth") << "Unknown exception during chat connection disconnect." << LL_ENDL; + } + } + if (mInstantMessageConnection.connected()) + { + try + { + mInstantMessageConnection.disconnect(); + } + catch (const std::exception& e) + { + LL_WARNS("FSPrimfeedAuth") << "Exception during instant message disconnect: " << e.what() << LL_ENDL; + } + catch (...) + { + LL_WARNS("FSPrimfeedAuth") << "Unknown exception during instant message disconnect." << LL_ENDL; + } + } +} + +// Factory method to create a shared pointer to FSPrimfeedAuth. +std::shared_ptr FSPrimfeedAuth::create(authorized_callback_t callback) +{ + // Ensure only one authentication attempt is in progress. + if (sPrimfeedAuth) + { + // Already in progress; return the existing instance. + return sPrimfeedAuth; + } + auto auth = std::shared_ptr(new FSPrimfeedAuth(callback)); + if(!auth) + { + return nullptr; + } + + auth->mAuthenticating = true; + + // If no token stored, begin the login request; otherwise check user status. + if (gSavedPerAccountSettings.getString("FSPrimfeedOAuthToken").empty()) + { + auth->beginLoginRequest(); + } + else + { + auth->checkUserStatus(); + } + return auth; +} + +void FSPrimfeedAuth::beginLoginRequest() +{ + // Get our API key and user UUID. + std::string viewer_api_key = gSavedSettings.getString("FSPrimfeedViewerApiKey"); + std::string user_uuid = gAgent.getID().asString(); + + std::string url = "https://api.primfeed.com/pf/viewer/create-login-request"; + std::string post_data = ""; // No body parameters required. + + // Create the headers object. + LLCore::HttpHeaders::ptr_t pHeader(new LLCore::HttpHeaders()); + LLCore::HttpOptions::ptr_t options(new LLCore::HttpOptions()); + + pHeader->append("pf-viewer-api-key", viewer_api_key); + pHeader->append("pf-user-uuid", user_uuid); + + // Set up HTTP options + options->setWantHeaders(true); + options->setRetries(0); + options->setTimeout(PRIMFEED_CONNECT_TIMEOUT); + + // Capture shared_ptr to self + auto self = shared_from_this(); + + const auto end(pHeader->end()); + for (auto it(pHeader->begin()); end != it; ++it) + { + LL_DEBUGS("Primfeed") << "Header: " << it->first << " = " << it->second << LL_ENDL; + } + + // Pass both success and failure callbacks + FSCoreHttpUtil::callbackHttpPostRaw( + url, + post_data, + [self](LLSD const &aData) { + LL_DEBUGS("FSPrimfeedAuth") << "Login request response(OK): " << aData << LL_ENDL; + FSPrimfeedAuthResponse(aData, + [self](bool success, const LLSD &response) { + self->gotRequestId(success, response); + } + ); + }, + [self](LLSD const &aData) { + LL_DEBUGS("FSPrimfeedAuth") << "Login request response(FAIL): " << aData << LL_ENDL; + FSPrimfeedAuthResponse(aData, + [self](bool success, const LLSD &response) { + self->gotRequestId(success, response); + } + ); + }, + pHeader, + options + ); +} + +void FSPrimfeedAuth::gotRequestId(bool success, const LLSD &response) +{ + if (!success) + { + LLNotificationsUtil::add("PrimfeedLoginRequestFailed"); + mCallback(false, LLSD()); + return; + } + mRequestId = response["requestId"].asString(); + if (mRequestId.empty()) + { + LLNotificationsUtil::add("PrimfeedLoginRequestFailed"); + mCallback(false, LLSD()); + return; + } + // Open the browser for user approval. + std::string viewer_api_key = gSavedSettings.getString("FSPrimfeedViewerApiKey"); + std::string auth_url = "https://www.primfeed.com/oauth/viewer?r=" + mRequestId + "&v=" + viewer_api_key; + gViewerWindow->getWindow()->spawnWebBrowser(auth_url, true); + +} + +/// This function is called by the chat interceptor when the message +/// "#PRIMFEED_OAUTH: " is intercepted. +void FSPrimfeedAuth::onOauthTokenReceived(const std::string_view& oauth_token) +{ + if (oauth_token.empty()) + { + mCallback(false, LLSD()); + return; + } + mOauthToken = oauth_token; + validateRequest(); +} + +void FSPrimfeedAuth::onChatMessage(const LLSD& message) +{ + constexpr std::string_view oauth_msg_prefix = "#PRIMFEED_OAUTH: "; + const std::string msg = message["message"].asString(); + if (msg.find(std::string(oauth_msg_prefix)) == 0) + { + std::string_view oauth_token(msg.data() + oauth_msg_prefix.size(), msg.size() - oauth_msg_prefix.size()); + LL_DEBUGS("Primfeed") << "Received OAuth token: " << msg << "extracted:<" << oauth_token << ">" << LL_ENDL; + onOauthTokenReceived(oauth_token); + } +} + + +void FSPrimfeedAuth::validateRequest() +{ + // No POST body needed. + std::string post_data = ""; + std::string url = "https://api.primfeed.com/pf/viewer/validate-request"; + + // Retrieve the viewer API key. + std::string viewer_api_key = gSavedSettings.getString("FSPrimfeedViewerApiKey"); + + // Create and populate the headers. + LLCore::HttpHeaders::ptr_t pHeader(new LLCore::HttpHeaders()); + pHeader->append("Authorization", "Bearer " + mOauthToken); + pHeader->append("pf-viewer-api-key", viewer_api_key); + pHeader->append("pf-viewer-request-id", mRequestId); + + // Set HTTP options + LLCore::HttpOptions::ptr_t options(new LLCore::HttpOptions()); + options->setWantHeaders(true); + options->setRetries(0); + options->setTimeout(PRIMFEED_CONNECT_TIMEOUT); + + // print out pHeader for debuging using iterating over pHeader and using LL_DEBUGS + const auto end(pHeader->end()); + for (auto it(pHeader->begin()); end != it; ++it) + { + LL_DEBUGS("Primfeed") << "Header: " << it->first << " = " << it->second << LL_ENDL; + } + + auto self = shared_from_this(); + try + { + FSCoreHttpUtil::callbackHttpPostRaw( + url, + post_data, + [self](LLSD const &aData) { + LL_DEBUGS("FSPrimfeedAuth") << "Validation-request response(OK): " << aData << LL_ENDL; + FSPrimfeedAuthResponse(aData, + [self](bool success, const LLSD &response) { + self->gotValidateResponse(success, response); + } + ); + }, + [self](LLSD const &aData) { + LL_INFOS("FSPrimfeedAuth") << "Validation-request response(FAIL): " << aData << LL_ENDL; + FSPrimfeedAuthResponse(aData, + [self](bool success, const LLSD &response) { + self->gotValidateResponse(success, response); + } + ); + }, + pHeader, + options + ); + } + catch(const std::exception& e) + { + LL_WARNS("Primfeed") << "Primfeed validation failed " << e.what() << LL_ENDL; + } + +} + + +void FSPrimfeedAuth::gotValidateResponse(bool success, const LLSD &response) +{ + if (!success) + { + LLNotificationsUtil::add("PrimfeedValidateFailed"); + mCallback(false, response); + return; + } + checkUserStatus(); +} + +void FSPrimfeedAuth::checkUserStatus() +{ + std::string viewer_api_key = gSavedSettings.getString("FSPrimfeedViewerApiKey"); + + // Build the base URL without query parameters. + std::string url = "https://api.primfeed.com/pf/viewer/user"; + LL_DEBUGS("Primfeed") << "URL: " << url << LL_ENDL; + + // Create and populate the headers. + LLCore::HttpHeaders::ptr_t pHeader(new LLCore::HttpHeaders()); + pHeader->append("Authorization", "Bearer " + mOauthToken); + pHeader->append("pf-viewer-api-key", viewer_api_key); + + // Set HTTP options. + LLCore::HttpOptions::ptr_t options(new LLCore::HttpOptions()); + options->setWantHeaders(true); + options->setRetries(0); + options->setTimeout(PRIMFEED_CONNECT_TIMEOUT); + + // Make the HTTP GET request, passing in the headers and options. + FSCoreHttpUtil::callbackHttpGetRaw( + url, + [this](LLSD const &aData) { + LL_DEBUGS("FSPrimfeedAuth") << "Check-user-status response: " << aData << LL_ENDL; + FSPrimfeedAuthResponse(aData, [this](bool success, const LLSD &response) { + this->gotUserStatus(success, response); + }); + }, + [this](LLSD const &aData) { + LL_INFOS("FSPrimfeedAuth") << "Check-user-status response (failure): " << aData << LL_ENDL; + // Optionally, call the same processing for failure or handle separately. + FSPrimfeedAuthResponse(aData, [this](bool success, const LLSD &response){ + this->gotUserStatus(success, response); + }); + }, + pHeader, + options + ); +} + + +void FSPrimfeedAuth::gotUserStatus(bool success, const LLSD &response) +{ + LL_INFOS("Primfeed") << "User status: " << response << "(" << success << ")" << LL_ENDL; + if (success && response.has("plan")) + { + gSavedPerAccountSettings.setString("FSPrimfeedOAuthToken", mOauthToken); + gSavedPerAccountSettings.setString("FSPrimfeedRequestId", mRequestId); + gSavedPerAccountSettings.setString("FSPrimfeedPlan", response["plan"].asString()); + gSavedPerAccountSettings.setString("FSPrimfeedProfileLink", response["link"].asString()); + gSavedPerAccountSettings.setString("FSPrimfeedUsername", response["username"].asString()); + FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_CONNECTED); + mCallback(true, response); + } + else + { + LLNotificationsUtil::add("PrimfeedUserStatusFailed"); + FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_DISCONNECTED); + mCallback(false, response); + } +} diff --git a/indra/newview/fsprimfeedauth.h b/indra/newview/fsprimfeedauth.h new file mode 100644 index 0000000000..a3e07ca6eb --- /dev/null +++ b/indra/newview/fsprimfeedauth.h @@ -0,0 +1,93 @@ +/** +* @file fsprimfeedauth.h +* @brief Primfeed Authorisation workflow 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$ +*/ +#ifndef FSPRIMFEEDAUTH_H +#define FSPRIMFEEDAUTH_H + +#include "llsd.h" +#include "llviewercontrol.h" +#include +#include + +/* +* Primfeed authentication workflow class. +* +* This class handles the Primfeed OAuth login flow and provides methods to +* check the user status and receive a callback when the authentication +* process is complete. +* based on the workflow documented at https://docs.primfeed.com/api/third-party-viewers +*/ +class FSPrimfeedAuth : public std::enable_shared_from_this +{ +public: + // Callback type: first parameter indicates success and the second holds any LLSD response. + using authorized_callback_t = std::function; + static std::shared_ptr create(authorized_callback_t callback); + static std::unique_ptr sPrimfeedAuthPump; + ~FSPrimfeedAuth(); + + // Should be called by the chat interceptor when an oauth token is received. + void onOauthTokenReceived(const std::string_view& oauth_token); + void onInstantMessage(const LLSD& message); + void onChatMessage(const LLSD& message); + + // Begin the login request flow. + void beginLoginRequest(); + // Check the user status. + void checkUserStatus(); + static bool isPendingAuth(){ return (sPrimfeedAuth != nullptr); } + static bool isAuthorized(){ return (!gSavedPerAccountSettings.getString("FSPrimfeedOAuthToken").empty()); } + static void initiateAuthRequest(); + static void resetAuthStatus(); + +private: + static std::shared_ptr sPrimfeedAuth; + + explicit FSPrimfeedAuth(authorized_callback_t callback); + authorized_callback_t mCallback; + bool mAuthenticating; + std::string mOauthToken; + std::string mRequestId; + + // Callback when a login request response is received. + void gotRequestId(bool success, const LLSD &response); + // Validate the login request. + void validateRequest(); + // Callback when the validate response is received. + void gotValidateResponse(bool success, const LLSD &response); + // Callback when the user status response is received. + void gotUserStatus(bool success, const LLSD &response); + + boost::signals2::connection mInstantMessageConnection; + boost::signals2::connection mChatMessageConnection; + // Static flag to prevent duplicate authentication attempts. + static std::atomic sAuthorisationInProgress; + + static constexpr U32 PRIMFEED_CONNECT_TIMEOUT = 300; // 5 minute timeout should work +}; + +#endif // FSPRIMFEEDAUTH_H \ No newline at end of file diff --git a/indra/newview/fsprimfeedconnect.cpp b/indra/newview/fsprimfeedconnect.cpp new file mode 100644 index 0000000000..f6d91480a2 --- /dev/null +++ b/indra/newview/fsprimfeedconnect.cpp @@ -0,0 +1,189 @@ +/** +* @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); +} \ No newline at end of file diff --git a/indra/newview/fsprimfeedconnect.h b/indra/newview/fsprimfeedconnect.h new file mode 100644 index 0000000000..409a93faed --- /dev/null +++ b/indra/newview/fsprimfeedconnect.h @@ -0,0 +1,82 @@ +/** +* @file fsprimfeedconect.h +* @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$ +*/ +#ifndef FS_PRIMFEEDCONNECT_H +#define FS_PRIMFEEDCONNECT_H + +#include "llsingleton.h" +#include "llsd.h" +#include "llimage.h" +#include "fsprimfeedauth.h" +#include "llcorehttputil.h" +#include "bufferarray.h" +#include "llcoros.h" +#include "llviewercontrol.h" // for gSavedSettings/gSavedPerAccountSettings +#include + +// Coro based connector designed to interface with floater designed along the same principles as LLFloaterFlickr.cpp + +class FSPrimfeedConnect : public LLSingleton +{ + LLSINGLETON(FSPrimfeedConnect); +public: + // Connection states for Primfeed operations + enum EConnectionState + { + PRIMFEED_DISCONNECTED = 0, + PRIMFEED_CONNECTING, + PRIMFEED_CONNECTED, + PRIMFEED_POSTING, + PRIMFEED_POSTED, + PRIMFEED_POST_FAILED, + PRIMFEED_DISCONNECTING + }; + + // Callback invoked on post completion: success flag and URL (empty on failure) + using post_callback_t = std::function; + + // Posts a snapshot to Primfeed; requires FSPrimfeedAuth::isAuthorized() + void uploadPhoto(const LLSD& params, LLImageFormatted* image, post_callback_t callback); + + // Retrieve and update account info from Primfeed (not used kept for compatibility) + void loadPrimfeedInfo(); + + void setConnectionState(EConnectionState state); + EConnectionState getConnectionState() const; + bool isTransactionOngoing() const; + +private: + // Internal coroutine entry-point for uploads + void uploadPhotoCoro(const LLSD& params, LLImageFormatted* image); + + // Cached callback until coroutine completes + post_callback_t mPostCallback; + + // Current connection/post state + EConnectionState mConnectionState = PRIMFEED_DISCONNECTED; +}; +#endif // FS_PRIMFEEDCONNECT_H \ No newline at end of file diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 190e02b769..910efcd775 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -235,6 +235,7 @@ #include "llfloatersimplesnapshot.h" #include "llfloatersnapshot.h" #include "llfloaterflickr.h" +#include "fsfloaterprimfeed.h" // Primfeed Floater #include "llsidepanelinventory.h" #include "llatmosphere.h" @@ -1751,6 +1752,7 @@ bool LLAppViewer::doFrame() LLFloaterSnapshot::update(); // take snapshots LLFloaterSimpleSnapshot::update(); LLFloaterFlickr::update(); // FIRE-35002 - Flickr preview not updating whne opened directly from tool tray icon + FSFloaterPrimfeed::update(); // Primfeed support gGLActive = false; } diff --git a/indra/newview/llfloatersnapshot.cpp b/indra/newview/llfloatersnapshot.cpp index ac545cfe87..10e9fd3d5e 100644 --- a/indra/newview/llfloatersnapshot.cpp +++ b/indra/newview/llfloatersnapshot.cpp @@ -30,6 +30,7 @@ #include "llfloaterreg.h" #include "llfloaterflickr.h" // Share to Flickr +#include "fsfloaterprimfeed.h" // Share to Primfeed #include "llimagefiltersmanager.h" #include "llcheckboxctrl.h" #include "llcombobox.h" @@ -1485,12 +1486,12 @@ bool LLFloaterSnapshot::isWaitingState() // FIRE-35002 - Post to flickr broken, improved solution // bool LLFloaterSnapshotBase::ImplBase::updatePreviewList(bool initialized) -bool LLFloaterSnapshotBase::ImplBase::updatePreviewList(bool initialized, bool have_flickr) +bool LLFloaterSnapshotBase::ImplBase::updatePreviewList(bool initialized, bool have_socials) // { // Share to Flickr //if (!initialized) - if (!initialized && !have_flickr) + if (!initialized && !have_socials) // return false; @@ -1509,16 +1510,18 @@ void LLFloaterSnapshotBase::ImplBase::updateLivePreview() { // don't update preview for hidden floater // FIRE-35002 - Post to flickr broken - LLFloaterFlickr* floater_flickr = LLFloaterReg::findTypedInstance("flickr"); - auto have_flickr = floater_flickr != nullptr; + bool have_socials = ( + LLFloaterReg::findTypedInstance("flickr") != nullptr || + LLFloaterReg::findTypedInstance("primfeed") != nullptr + ); if ( ((mFloater && mFloater->isInVisibleChain()) || - have_flickr) && - ImplBase::updatePreviewList(true, have_flickr)) + have_socials) && + ImplBase::updatePreviewList(true, have_socials)) // { LL_DEBUGS() << "changed" << LL_ENDL; updateControls(mFloater); - } + } } //static diff --git a/indra/newview/llfloatersnapshot.h b/indra/newview/llfloatersnapshot.h index e6c4025bb3..934893b50b 100644 --- a/indra/newview/llfloatersnapshot.h +++ b/indra/newview/llfloatersnapshot.h @@ -123,7 +123,7 @@ public: virtual EStatus getStatus() const { return mStatus; } virtual void setNeedRefresh(bool need); - static bool updatePreviewList(bool initialized, bool have_flickr = false); // FIRE-35002 - Post to flickr broken, improved solution + static bool updatePreviewList(bool initialized, bool have_socials = false); // FIRE-35002 - Post to flickr broken, improved solution void setAdvanced(bool advanced) { mAdvanced = advanced; } void setSkipReshaping(bool skip) { mSkipReshaping = skip; } diff --git a/indra/newview/llpanelsnapshotoptions.cpp b/indra/newview/llpanelsnapshotoptions.cpp index bcc0460ceb..3864a427fc 100644 --- a/indra/newview/llpanelsnapshotoptions.cpp +++ b/indra/newview/llpanelsnapshotoptions.cpp @@ -32,6 +32,7 @@ #include "llfloatersnapshot.h" // FIXME: create a snapshot model #include "llfloaterreg.h" #include "llfloaterflickr.h" // Share to Flickr +#include "fsfloaterprimfeed.h" // Share to Primfeed /** * Provides several ways to save a snapshot. @@ -53,6 +54,7 @@ private: void onSaveToInventory(); void onSaveToComputer(); void onSendToFlickr(); // Share to Flickr + void onSendToPrimfeed(); // Share to Primfeed LLFloaterSnapshotBase* mSnapshotFloater; }; @@ -66,6 +68,7 @@ LLPanelSnapshotOptions::LLPanelSnapshotOptions() mCommitCallbackRegistrar.add("Snapshot.SaveToInventory", boost::bind(&LLPanelSnapshotOptions::onSaveToInventory, this)); mCommitCallbackRegistrar.add("Snapshot.SaveToComputer", boost::bind(&LLPanelSnapshotOptions::onSaveToComputer, this)); mCommitCallbackRegistrar.add("Snapshot.SendToFlickr", boost::bind(&LLPanelSnapshotOptions::onSendToFlickr, this)); // Share to Flickr + mCommitCallbackRegistrar.add("Snapshot.SendToPrimfeed", boost::bind(&LLPanelSnapshotOptions::onSendToPrimfeed, this)); // Share to Primfeed } // virtual @@ -122,3 +125,17 @@ void LLPanelSnapshotOptions::onSendToFlickr() LLFloaterReg::showInstance("flickr"); } // + +// Share to Primfeed +void LLPanelSnapshotOptions::onSendToPrimfeed() +{ + LLFloaterReg::hideInstance("snapshot"); + + auto* primfeed_floater = dynamic_cast(LLFloaterReg::getInstance("primfeed")); + if (primfeed_floater) + { + primfeed_floater->showPhotoPanel(); + } + LLFloaterReg::showInstance("primfeed"); +} +// \ No newline at end of file diff --git a/indra/newview/llsnapshotlivepreview.cpp b/indra/newview/llsnapshotlivepreview.cpp index c9826a9775..80ccde3483 100644 --- a/indra/newview/llsnapshotlivepreview.cpp +++ b/indra/newview/llsnapshotlivepreview.cpp @@ -36,6 +36,7 @@ #include "llfloaterperms.h" #include "llfloaterreg.h" #include "llfloaterflickr.h" // Share to Flickr +#include "fsfloaterprimfeed.h" // Share to Primfeed #include "llimagefilter.h" #include "llimagefiltersmanager.h" #include "llimagebmp.h" diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 4fc57172d6..06f68e9407 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -225,6 +225,7 @@ #include "lggbeamcolormapfloater.h" #include "lggbeammapfloater.h" #include "llfloaterdisplayname.h" +#include "fsfloaterprimfeed.h" #include "llfloaterflickr.h" #include "llfloaterscriptrecover.h" #include "llfloatersearchreplace.h" @@ -630,6 +631,7 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("export_collada", "floater_export_collada.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("delete_queue", "floater_script_queue.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("flickr", "floater_flickr.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); + LLFloaterReg::add("primfeed", "floater_primfeed.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("fs_asset_blacklist", "floater_fs_asset_blacklist.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("fs_avatar_render_settings", "floater_fs_avatar_render_settings.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("fs_blocklist", "floater_fs_blocklist.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index a85a7e8de2..382b211c82 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -2757,6 +2757,28 @@ class LLAdvancedCompressFileTest : public view_listener_t } }; +// Primfeed integration test functions (can be removed when the feature is stable) +/////////////////// +// PRIMFEED AUTH // +/////////////////// +#include "fsprimfeedauth.h" +class LLAdvancedPrimfeedAuth : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + FSPrimfeedAuth::initiateAuthRequest(); + return true; + } +}; +class LLAdvancedPrimfeedAuthReset : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + FSPrimfeedAuth::resetAuthStatus(); + return true; + } +}; +// ///////////////////////// // SHOW DEBUG SETTINGS // @@ -12809,6 +12831,8 @@ void initialize_menus() view_listener_t::addMenu(new LLAdvancedCheckShowObjectUpdates(), "Advanced.CheckShowObjectUpdates"); view_listener_t::addMenu(new LLAdvancedCompressImage(), "Advanced.CompressImage"); view_listener_t::addMenu(new LLAdvancedCompressFileTest(), "Advanced.CompressFileTest"); + view_listener_t::addMenu(new LLAdvancedPrimfeedAuth(), "Advanced.PrimfeedAuth"); + view_listener_t::addMenu(new LLAdvancedPrimfeedAuthReset(), "Advanced.PrimfeedAuthReset"); view_listener_t::addMenu(new LLAdvancedShowDebugSettings(), "Advanced.ShowDebugSettings"); view_listener_t::addMenu(new LLAdvancedEnableViewAdminOptions(), "Advanced.EnableViewAdminOptions"); view_listener_t::addMenu(new LLAdvancedToggleViewAdminOptions(), "Advanced.ToggleViewAdminOptions"); diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 16d0982555..76db9bfff9 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -111,6 +111,7 @@ #include "llfloatertools.h" #include "llfloatersnapshot.h" // for snapshotFrame #include "llfloaterflickr.h" // for snapshotFrame +#include "fsfloaterprimfeed.h" // for snapshotFrame #include "llsnapshotlivepreview.h" // for snapshotFrame // #include "llpanelface.h" // switchable edit texture/materials panel - include not needed #include "llpathfindingpathtool.h" @@ -8047,12 +8048,12 @@ bool LLPipeline::renderSnapshotFrame(LLRenderTarget* src, LLRenderTarget* dst) } const bool simple_snapshot_visible = LLFloaterReg::instanceVisible("simple_snapshot"); const bool flickr_snapshot_visible = LLFloaterReg::instanceVisible("flickr"); + const bool primfeed_snapshot_visible = LLFloaterReg::instanceVisible("primfeed"); // Primfeed integration const bool snapshot_visible = LLFloaterReg::instanceVisible("snapshot"); - const bool any_snapshot_visible = simple_snapshot_visible || flickr_snapshot_visible || snapshot_visible; + const bool any_snapshot_visible = simple_snapshot_visible || flickr_snapshot_visible || primfeed_snapshot_visible || snapshot_visible; // Primfeed integration if (!show_frame || !any_snapshot_visible || !gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI)) { return false; - } LLSnapshotLivePreview * previewView = nullptr; if (snapshot_visible) @@ -8066,6 +8067,13 @@ bool LLPipeline::renderSnapshotFrame(LLRenderTarget* src, LLRenderTarget* dst) auto * floater = dynamic_cast(LLFloaterReg::findInstance("flickr")); previewView = floater->getPreviewView(); } + // Primfeed integration + if (primfeed_snapshot_visible && !previewView) + { + auto * floater = dynamic_cast(LLFloaterReg::findInstance("primfeed")); + previewView = floater->getPreviewView(); + } + // if(!previewView) { return false; diff --git a/indra/newview/skins/default/textures/icons/primfeed.png b/indra/newview/skins/default/textures/icons/primfeed.png new file mode 100644 index 0000000000000000000000000000000000000000..6b8dd91da9a92de6245567328b6fb0f56a54cf35 GIT binary patch literal 3246 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 + diff --git a/indra/newview/skins/default/xui/en/floater_primfeed.xml b/indra/newview/skins/default/xui/en/floater_primfeed.xml new file mode 100644 index 0000000000..fa06d62c1f --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_primfeed.xml @@ -0,0 +1,90 @@ + + + + + + + + + + Error + + + + Loading... + + + + diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 46995fe18a..9623be67ef 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -5669,6 +5669,18 @@ + + + + + + [MESSAGE] + + + [MESSAGE] + Your snapshot can now be viewed [https://www.flickr.com/photos/me/[ID] here]. + + +Your primfeed post can now be viewed [[PF_POSTURL] here]. @@ -14676,4 +14689,60 @@ https://wiki.firestormviewer.org/antivirus_whitelisting yestext="Okay"/> + +Login request denied by Primfeed. + + +Primfeed authorisation failed. The authorisation sequence was not completed. + + +Primfeed authorisation is already in progress. Please complete the primfeed authorisation in your web browser before trying again. + + +Primfeed authorisation completed. You may now post images to Primfeed. + + +Primfeed user validation failed. Primfeed did not recognise this account, or the login failed. + + +You have already linked this account to Primfeed. Use the reset button if you wish to start over. + + +Primfeed user login successful, but status checks have failed. Please check the Primfeed is working. + diff --git a/indra/newview/skins/default/xui/en/panel_primfeed_account.xml b/indra/newview/skins/default/xui/en/panel_primfeed_account.xml new file mode 100644 index 0000000000..0ad4e85db0 --- /dev/null +++ b/indra/newview/skins/default/xui/en/panel_primfeed_account.xml @@ -0,0 +1,105 @@ + + + + + + Not connected to Primfeed. + + + + Account type: + + + + + + + + [https://docs.primfeed.com Learn more about Primfeed] + + + diff --git a/indra/newview/skins/default/xui/en/panel_primfeed_photo.xml b/indra/newview/skins/default/xui/en/panel_primfeed_photo.xml new file mode 100644 index 0000000000..237fbbf3e0 --- /dev/null +++ b/indra/newview/skins/default/xui/en/panel_primfeed_photo.xml @@ -0,0 +1,350 @@ + + + + + + + + + + + + + + + + + x + + + + + + + Refreshing... + + + + + + + + Description: + + + + + + + + \ No newline at end of file diff --git a/indra/newview/skins/default/xui/en/panel_snapshot_options.xml b/indra/newview/skins/default/xui/en/panel_snapshot_options.xml index d6f10ff08b..b7fa508494 100644 --- a/indra/newview/skins/default/xui/en/panel_snapshot_options.xml +++ b/indra/newview/skins/default/xui/en/panel_snapshot_options.xml @@ -52,6 +52,7 @@ height="22" image_overlay="Snapshot_Inventory" image_overlay_alignment="left" + image_overlay_width="16" image_top_pad="-1" imgoverlay_label_space="10" label="Save to Inventory" @@ -111,6 +112,30 @@ function="Snapshot.SendToFlickr"/> + + + Problem connecting to Flickr Problem posting to Flickr Problem disconnecting from Flickr + Connecting to Primfeed... + Not Authorized... + Posting... + Problem posting to Primfeed Black & White @@ -2767,6 +2771,8 @@ name="Command_360_Capture_Label">360° Snapshot Beacons Poser Pose your avatar and animated objects + Primfeed + Post directly to your Primfeed account. Capture a 360° equirectangular image