From 23f4a204eada7f576b5a6d0833f8569a90031d41 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:00:18 +0200 Subject: [PATCH 1/3] Added API code for FeedReader --- plugins/FeedReader/CMakeLists.txt | 17 +- plugins/FeedReader/FeedReader.pro | 10 +- plugins/FeedReader/FeedReaderPlugin.cpp | 43 +++ plugins/FeedReader/FeedReaderPlugin.h | 2 + plugins/FeedReader/interface/rsFeedReader.h | 6 + .../FeedReader/services/FeedReaderJsonApi.cpp | 339 ++++++++++++++++++ .../FeedReader/services/FeedReaderJsonApi.h | 42 +++ 7 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 plugins/FeedReader/services/FeedReaderJsonApi.cpp create mode 100644 plugins/FeedReader/services/FeedReaderJsonApi.h diff --git a/plugins/FeedReader/CMakeLists.txt b/plugins/FeedReader/CMakeLists.txt index 1497e0fc4..c78e1deab 100644 --- a/plugins/FeedReader/CMakeLists.txt +++ b/plugins/FeedReader/CMakeLists.txt @@ -17,6 +17,7 @@ set(CMAKE_AUTOUIC ON) set(FEEDREADER_SOURCES FeedReaderPlugin.cpp services/p3FeedReader.cc + services/FeedReaderJsonApi.cpp services/p3FeedReaderThread.cc services/rsFeedReaderItems.cc gui/FeedReaderDialog.cpp @@ -105,7 +106,7 @@ if(WIN32) # (Linux) the loader resolves these symbols from the hosting retroshare-gui # process at load time, so this explicit link is only needed on Windows. target_link_libraries(FeedReader PRIVATE retroshare retroshare-gui) - target_compile_definitions(FeedReader PRIVATE WINDOWS_SYS) + target_compile_definitions(FeedReader PRIVATE WINDOWS_SYS WIN_DLL_EXPORT) endif() target_link_libraries(FeedReader PRIVATE @@ -123,6 +124,13 @@ target_link_libraries(FeedReader PRIVATE target_compile_options(FeedReader PRIVATE "-Wno-deprecated-declarations") target_compile_definitions(FeedReader PRIVATE RS_NO_WARN_DEPRECATED) +# RapidJSON 1.1 contains an intentionally unusable assignment operator for +# GenericStringRef. GCC 15 diagnoses its body eagerly unless this warning is +# disabled (older GCC versions do not know this option). +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15) + target_compile_options(FeedReader PRIVATE "-Wno-template-body") +endif() + if(RS_DEVELOPMENT_BUILD) target_compile_options(FeedReader PRIVATE "-g" "-O0") endif() @@ -138,7 +146,14 @@ target_include_directories(FeedReader PRIVATE ${CMAKE_SOURCE_DIR}/retroshare-gui/src ${CMAKE_SOURCE_DIR}/retroshare-gui/src/gui ${CMAKE_SOURCE_DIR}/supportlibs/rapidjson/include + ${CMAKE_SOURCE_DIR}/supportlibs/restbed/include ) +if(TARGET restbed-static) + target_link_libraries(FeedReader PRIVATE restbed-static) +elseif(TARGET restbed-shared) + target_link_libraries(FeedReader PRIVATE restbed-shared) +endif() + # --- Installation --- install(TARGETS FeedReader DESTINATION lib/retroshare/plugins) diff --git a/plugins/FeedReader/FeedReader.pro b/plugins/FeedReader/FeedReader.pro index 5201c5b47..b0759583c 100644 --- a/plugins/FeedReader/FeedReader.pro +++ b/plugins/FeedReader/FeedReader.pro @@ -30,9 +30,15 @@ greaterThan(QT_MAJOR_VERSION, 4) { greaterThan(QT_MAJOR_VERSION,5): QT += core5compat +# RapidJSON 1.1 triggers GCC 15's eager template-body diagnostic. +gcc:greaterThan(QMAKE_GCC_MAJOR_VERSION, 14): QMAKE_CXXFLAGS += -Wno-template-body + +INCLUDEPATH += ../../supportlibs/restbed/include + target.files = lib/libFeedReader.so SOURCES = FeedReaderPlugin.cpp \ + services/FeedReaderJsonApi.cpp \ services/p3FeedReader.cc \ services/p3FeedReaderThread.cc \ services/rsFeedReaderItems.cc \ @@ -54,6 +60,7 @@ SOURCES = FeedReaderPlugin.cpp \ util/XPathWrapper.cpp HEADERS = FeedReaderPlugin.h \ + services/FeedReaderJsonApi.h \ interface/rsFeedReader.h \ services/p3FeedReader.h \ services/p3FeedReaderThread.h \ @@ -117,10 +124,11 @@ linux-* { } win32 { - DEFINES += CURL_STATICLIB LIBXML_STATIC LIBXSLT_STATIC LIBEXSLT_STATIC + DEFINES += CURL_STATICLIB LIBXML_STATIC LIBXSLT_STATIC LIBEXSLT_STATIC WIN_DLL_EXPORT #Have to reorder libs, else got /libs/lib/libcrypto.a(bio_lib.o):bio_lib.c:(.text+0x0): multiple definition of `BIO_new' LIBS = -lcurl -lxml2 -lz -lxslt -lws2_32 -lwldap32 -lssl -lcrypto -lgdi32 $${LIBS} + LIBS += -L$$PWD/../../supportlibs/restbed -lrestbed isEmpty(QMAKE_SH) { # MinGW diff --git a/plugins/FeedReader/FeedReaderPlugin.cpp b/plugins/FeedReader/FeedReaderPlugin.cpp index bc44a3036..bc20ccd2b 100644 --- a/plugins/FeedReader/FeedReaderPlugin.cpp +++ b/plugins/FeedReader/FeedReaderPlugin.cpp @@ -30,6 +30,8 @@ #include "gui/FeedReaderConfig.h" #include "gui/FeedReaderFeedNotify.h" #include "services/p3FeedReader.h" +#include "services/FeedReaderJsonApi.h" +#include #include #include @@ -83,6 +85,7 @@ FeedReaderPlugin::FeedReaderPlugin() mFeedReader = NULL; mNotify = NULL; mFeedNotify = NULL; + mJsonApiProvider = NULL; Q_INIT_RESOURCE(FeedReader_images); Q_INIT_RESOURCE(FeedReader_qss); @@ -91,12 +94,41 @@ FeedReaderPlugin::FeedReaderPlugin() void FeedReaderPlugin::setInterfaces(RsPlugInInterfaces &interfaces) { mInterfaces = interfaces; + std::cerr << "FeedReader: JSON API interface pointer=" + << static_cast(mInterfaces.mJsonApi) << std::endl; mFeedReader = new p3FeedReader(mPlugInHandler, mInterfaces.mGxsForums, mInterfaces.mPosted); rsFeedReader = mFeedReader; mNotify = new FeedReaderNotify(); mFeedReader->setNotify(mNotify); + + if(mInterfaces.mJsonApi) + { + mJsonApiProvider = new FeedReaderJsonApi(*mFeedReader, *mInterfaces.mJsonApi); + mInterfaces.mJsonApi->registerResourceProvider(*mJsonApiProvider); + std::cerr << "FeedReader: JSON API provider registered=" + << mInterfaces.mJsonApi->hasResourceProvider(*mJsonApiProvider) + << std::endl; + // retroshare-service and Android may start JSON API before plugins are + // initialized, while the desktop GUI configures and starts it afterwards. + // Restart only an already-running server. In the GUI case the provider is + // picked up by the normal, later startupWebServices() call. + if(mInterfaces.mJsonApi->isRunning()) + { + const std::error_condition restartError = + mInterfaces.mJsonApi->restart(true); + std::cerr << "FeedReader: JSON API restart result=" + << restartError.value() << " (" << restartError.message() + << ")" << std::endl; + } + else + std::cerr << "FeedReader: JSON API startup deferred to core" + << std::endl; + } + else + std::cerr << "FeedReader: JSON API unavailable; routes not registered" + << std::endl; } ConfigPage *FeedReaderPlugin::qt_config_page() const @@ -123,6 +155,17 @@ FeedNotify *FeedReaderPlugin::qt_feedNotify() void FeedReaderPlugin::stop() { + if(mJsonApiProvider) + { + if(mInterfaces.mJsonApi) + { + mInterfaces.mJsonApi->unregisterResourceProvider(*mJsonApiProvider); + if(mInterfaces.mJsonApi->isRunning()) + mInterfaces.mJsonApi->restart(true); + } + delete mJsonApiProvider; + mJsonApiProvider = NULL; + } if (mFeedReader) { mFeedReader->setNotify(NULL); mFeedReader->stop(); diff --git a/plugins/FeedReader/FeedReaderPlugin.h b/plugins/FeedReader/FeedReaderPlugin.h index e7d8653f1..fbdda2481 100644 --- a/plugins/FeedReader/FeedReaderPlugin.h +++ b/plugins/FeedReader/FeedReaderPlugin.h @@ -28,6 +28,7 @@ class p3FeedReader; class FeedReaderNotify; class RsForums; +class FeedReaderJsonApi; class FeedReaderPlugin: public RsPlugin { @@ -65,4 +66,5 @@ private: mutable MainPage *mainpage; mutable QIcon *mIcon; mutable FeedNotify *mFeedNotify; + mutable FeedReaderJsonApi *mJsonApiProvider; }; diff --git a/plugins/FeedReader/interface/rsFeedReader.h b/plugins/FeedReader/interface/rsFeedReader.h index 326d3e022..6bf598bd9 100644 --- a/plugins/FeedReader/interface/rsFeedReader.h +++ b/plugins/FeedReader/interface/rsFeedReader.h @@ -99,11 +99,14 @@ public: public: FeedInfo() { + feedId = 0; + parentId = 0; proxyPort = 0; updateInterval = 0; lastUpdate = 0; storageTime = 0; errorState = RS_FEED_ERRORSTATE_OK; + workstate = WAITING; flag.folder = false; flag.infoFromFeed = false; flag.standardStorageTime = false; @@ -115,6 +118,9 @@ public: flag.updateForumInfo = false; flag.posted = false; flag.updatePostedInfo = false; + flag.postedFirstImage = false; + flag.postedOnlyImage = false; + flag.postedShrinkImage = false; flag.embedImages = false; flag.saveCompletePage = false; flag.preview = false; diff --git a/plugins/FeedReader/services/FeedReaderJsonApi.cpp b/plugins/FeedReader/services/FeedReaderJsonApi.cpp new file mode 100644 index 000000000..bcd55f8d1 --- /dev/null +++ b/plugins/FeedReader/services/FeedReaderJsonApi.cpp @@ -0,0 +1,339 @@ +/******************************************************************************* + * plugins/FeedReader/services/FeedReaderJsonApi.cpp * + * * + * Copyright (C) 2026 RetroShare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * This program 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "FeedReaderJsonApi.h" + +#include +#include + +#include +#include +#include + +#include "interface/rsFeedReader.h" +#include "retroshare/rsjsonapi.h" +#include "util/radix64.h" + +namespace +{ +using Document = rapidjson::Document; +using Value = rapidjson::Value; +using Allocator = Document::AllocatorType; + +const std::multimap jsonHeaders = { + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Headers", "Authorization, Content-Type"}, + {"Access-Control-Allow-Methods", "POST, OPTIONS"}, + {"Content-Type", "application/json"} +}; + +void closeJson(const std::shared_ptr& session, int status, const Document& doc) +{ + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + doc.Accept(writer); + auto headers = jsonHeaders; + headers.emplace("Content-Length", std::to_string(buffer.GetSize())); + session->close(status, buffer.GetString(), headers); +} + +void closeError(const std::shared_ptr& session, int status, const std::string& message) +{ + Document doc; + doc.SetObject(); + doc.AddMember("ok", false, doc.GetAllocator()); + doc.AddMember("error", Value(message.c_str(), doc.GetAllocator()), doc.GetAllocator()); + closeJson(session, status, doc); +} + +bool authenticate(const std::shared_ptr& session, RsJsonApi& jsonApi) +{ + std::istringstream header(session->get_request()->get_header("Authorization")); + std::string scheme, encoded; + header >> scheme >> encoded; + if(scheme != "Basic" || encoded.empty()) return false; + + const auto decoded = Radix64::decode(encoded); + if(decoded.empty()) return false; + const std::string token(reinterpret_cast(decoded.data()), decoded.size()); + return jsonApi.isAuthTokenValid(token); +} + +void addString(Value& object, const char* name, const std::string& value, Allocator& allocator) +{ + object.AddMember(Value(name, allocator), Value(value.c_str(), value.size(), allocator), allocator); +} + +Value feedToJson(const FeedInfo& feed, Allocator& allocator) +{ + Value out(rapidjson::kObjectType); + out.AddMember("feedId", feed.feedId, allocator); + out.AddMember("parentId", feed.parentId, allocator); + addString(out, "url", feed.url, allocator); + addString(out, "name", feed.name, allocator); + addString(out, "description", feed.description, allocator); + addString(out, "icon", feed.icon, allocator); + out.AddMember("updateInterval", feed.updateInterval, allocator); + out.AddMember("lastUpdate", static_cast(feed.lastUpdate), allocator); + out.AddMember("storageTime", feed.storageTime, allocator); + out.AddMember("workState", static_cast(feed.workstate), allocator); + out.AddMember("errorState", static_cast(feed.errorState), allocator); + addString(out, "errorString", feed.errorString, allocator); + out.AddMember("folder", feed.flag.folder, allocator); + out.AddMember("deactivated", feed.flag.deactivated, allocator); + out.AddMember("authentication", feed.flag.authentication, allocator); + return out; +} + +Value messageToJson(const FeedMsgInfo& msg, Allocator& allocator) +{ + Value out(rapidjson::kObjectType); + addString(out, "msgId", msg.msgId, allocator); + out.AddMember("feedId", msg.feedId, allocator); + addString(out, "title", msg.title, allocator); + addString(out, "link", msg.link, allocator); + addString(out, "author", msg.author, allocator); + addString(out, "description", msg.description, allocator); + addString(out, "descriptionTransformed", msg.descriptionTransformed, allocator); + out.AddMember("pubDate", static_cast(msg.pubDate), allocator); + addString(out, "attachmentLink", msg.attachmentLink, allocator); + addString(out, "attachmentMimeType", msg.attachmentMimeType, allocator); + out.AddMember("isNew", msg.flag.isnew, allocator); + out.AddMember("read", msg.flag.read, allocator); + out.AddMember("deleted", msg.flag.deleted, allocator); + return out; +} + +bool uintMember(const Document& doc, const char* name, uint32_t& value) +{ + if(!doc.HasMember(name) || !doc[name].IsUint()) return false; + value = doc[name].GetUint(); + return true; +} + +bool stringMember(const Document& doc, const char* name, std::string& value) +{ + if(!doc.HasMember(name) || !doc[name].IsString()) return false; + value = doc[name].GetString(); + return true; +} + +template +std::shared_ptr makeResource( + const std::string& path, RsJsonApi& jsonApi, Handler handler ) +{ + auto resource = std::make_shared(); + resource->set_path(path); + resource->set_method_handler("OPTIONS", [](const std::shared_ptr session) + { + session->close(restbed::NO_CONTENT, jsonHeaders); + }); + resource->set_authentication_handler([jsonApiPtr = &jsonApi]( + const std::shared_ptr session, + const std::function)>& callback) + { + if( session->get_request()->get_method() == "OPTIONS" || + authenticate(session, *jsonApiPtr) ) callback(session); + else session->close(restbed::UNAUTHORIZED, jsonHeaders); + }); + resource->set_method_handler("POST", [handler](const std::shared_ptr session) + { + const auto size = session->get_request()->get_header("Content-Length", 0); + session->fetch(static_cast(size), [handler]( + const std::shared_ptr fetched, const restbed::Bytes& body) + { + Document request; + request.Parse(reinterpret_cast(body.data()), body.size()); + if(request.HasParseError() || !request.IsObject()) + { + closeError(fetched, restbed::BAD_REQUEST, "Invalid JSON request body"); + return; + } + handler(fetched, request); + }); + }); + return resource; +} +} + +FeedReaderJsonApi::FeedReaderJsonApi( + RsFeedReader& feedReader, RsJsonApi& jsonApi ): + mFeedReader(feedReader), mJsonApi(jsonApi) {} + +std::vector> FeedReaderJsonApi::getResources() const +{ + std::cerr << "FeedReader: constructing JSON API resources" << std::endl; + std::vector> resources; + auto resource = [this](const std::string& path, auto handler) + { + return makeResource(path, mJsonApi, handler); + }; + + resources.push_back(resource("/rsFeedReader/getFeeds", [this](auto session, const Document& request) + { + uint32_t parentId = 0; + if(request.HasMember("parentId") && !uintMember(request, "parentId", parentId)) + return closeError(session, restbed::BAD_REQUEST, "parentId must be an unsigned integer"); + std::list feeds; + mFeedReader.getFeedList(parentId, feeds); + Document response; + response.SetObject(); + response.AddMember("ok", true, response.GetAllocator()); + Value array(rapidjson::kArrayType); + for(const auto& feed: feeds) array.PushBack(feedToJson(feed, response.GetAllocator()), response.GetAllocator()); + response.AddMember("feeds", array, response.GetAllocator()); + closeJson(session, restbed::OK, response); + })); + + resources.push_back(resource("/rsFeedReader/getMessages", [this](auto session, const Document& request) + { + uint32_t feedId = 0; + if(!uintMember(request, "feedId", feedId)) return closeError(session, restbed::BAD_REQUEST, "feedId is required"); + std::list messages; + if(!mFeedReader.getFeedMsgList(feedId, messages)) return closeError(session, restbed::NOT_FOUND, "Feed not found"); + Document response; + response.SetObject(); + response.AddMember("ok", true, response.GetAllocator()); + Value array(rapidjson::kArrayType); + for(const auto& msg: messages) array.PushBack(messageToJson(msg, response.GetAllocator()), response.GetAllocator()); + response.AddMember("messages", array, response.GetAllocator()); + closeJson(session, restbed::OK, response); + })); + + resources.push_back(resource("/rsFeedReader/addFolder", [this](auto session, const Document& request) + { + uint32_t parentId = 0, feedId = 0; + std::string name; + if(!stringMember(request, "name", name) || name.empty()) return closeError(session, restbed::BAD_REQUEST, "name is required"); + if(request.HasMember("parentId") && !uintMember(request, "parentId", parentId)) return closeError(session, restbed::BAD_REQUEST, "Invalid parentId"); + const auto result = mFeedReader.addFolder(parentId, name, feedId); + Document response; + response.SetObject(); + response.AddMember("ok", result == RS_FEED_RESULT_SUCCESS, response.GetAllocator()); + response.AddMember("result", static_cast(result), response.GetAllocator()); + response.AddMember("feedId", feedId, response.GetAllocator()); + closeJson(session, result == RS_FEED_RESULT_SUCCESS ? restbed::OK : restbed::BAD_REQUEST, response); + })); + + resources.push_back(resource("/rsFeedReader/addFeed", [this](auto session, const Document& request) + { + FeedInfo feed; + uint32_t feedId = 0; + if(!stringMember(request, "url", feed.url) || feed.url.empty()) return closeError(session, restbed::BAD_REQUEST, "url is required"); + stringMember(request, "name", feed.name); + if(request.HasMember("parentId") && !uintMember(request, "parentId", feed.parentId)) return closeError(session, restbed::BAD_REQUEST, "Invalid parentId"); + if(request.HasMember("updateInterval")) uintMember(request, "updateInterval", feed.updateInterval); + else feed.flag.standardUpdateInterval = true; + if(request.HasMember("storageTime")) uintMember(request, "storageTime", feed.storageTime); + else feed.flag.standardStorageTime = true; + if(request.HasMember("deactivated") && request["deactivated"].IsBool()) feed.flag.deactivated = request["deactivated"].GetBool(); + if(request.HasMember("user") && request["user"].IsString()) feed.user = request["user"].GetString(); + if(request.HasMember("password") && request["password"].IsString()) feed.password = request["password"].GetString(); + feed.flag.authentication = !feed.user.empty(); + const auto result = mFeedReader.addFeed(feed, feedId); + Document response; + response.SetObject(); + response.AddMember("ok", result == RS_FEED_RESULT_SUCCESS, response.GetAllocator()); + response.AddMember("result", static_cast(result), response.GetAllocator()); + response.AddMember("feedId", feedId, response.GetAllocator()); + closeJson(session, result == RS_FEED_RESULT_SUCCESS ? restbed::OK : restbed::BAD_REQUEST, response); + })); + + resources.push_back(resource("/rsFeedReader/updateFeed", [this](auto session, const Document& request) + { + uint32_t feedId = 0; + if(!uintMember(request, "feedId", feedId)) return closeError(session, restbed::BAD_REQUEST, "feedId is required"); + FeedInfo feed; + if(!mFeedReader.getFeedInfo(feedId, feed)) return closeError(session, restbed::NOT_FOUND, "Feed not found"); + if(feed.flag.folder) + { + std::string name; + if(!stringMember(request, "name", name) || name.empty()) return closeError(session, restbed::BAD_REQUEST, "name is required"); + const auto result = mFeedReader.setFolder(feedId, name); + Document response; response.SetObject(); response.AddMember("ok", result == RS_FEED_RESULT_SUCCESS, response.GetAllocator()); + return closeJson(session, result == RS_FEED_RESULT_SUCCESS ? restbed::OK : restbed::BAD_REQUEST, response); + } + stringMember(request, "url", feed.url); + stringMember(request, "name", feed.name); + if(request.HasMember("updateInterval")) uintMember(request, "updateInterval", feed.updateInterval); + if(request.HasMember("storageTime")) uintMember(request, "storageTime", feed.storageTime); + if(request.HasMember("deactivated") && request["deactivated"].IsBool()) feed.flag.deactivated = request["deactivated"].GetBool(); + if(request.HasMember("user") && request["user"].IsString()) feed.user = request["user"].GetString(); + if(request.HasMember("password") && request["password"].IsString()) feed.password = request["password"].GetString(); + feed.flag.authentication = !feed.user.empty(); + const auto result = mFeedReader.setFeed(feedId, feed); + Document response; response.SetObject(); response.AddMember("ok", result == RS_FEED_RESULT_SUCCESS, response.GetAllocator()); + response.AddMember("result", static_cast(result), response.GetAllocator()); + closeJson(session, result == RS_FEED_RESULT_SUCCESS ? restbed::OK : restbed::BAD_REQUEST, response); + })); + + auto idAction = [this, &resource](const std::string& path, auto action) + { + return resource(path, [this, action](auto session, const Document& request) + { + uint32_t feedId = 0; + if(!uintMember(request, "feedId", feedId)) return closeError(session, restbed::BAD_REQUEST, "feedId is required"); + const bool ok = action(feedId); + Document response; response.SetObject(); response.AddMember("ok", ok, response.GetAllocator()); + closeJson(session, ok ? restbed::OK : restbed::NOT_FOUND, response); + }); + }; + resources.push_back(idAction("/rsFeedReader/removeFeed", [this](uint32_t id){ return mFeedReader.removeFeed(id); })); + resources.push_back(idAction("/rsFeedReader/refreshFeed", [this](uint32_t id){ return mFeedReader.processFeed(id); })); + + resources.push_back(resource("/rsFeedReader/setMessageRead", [this](auto session, const Document& request) + { + uint32_t feedId = 0; std::string msgId; + if(!uintMember(request, "feedId", feedId) || !stringMember(request, "msgId", msgId) || !request.HasMember("read") || !request["read"].IsBool()) + return closeError(session, restbed::BAD_REQUEST, "feedId, msgId and read are required"); + const bool ok = mFeedReader.setMessageRead(feedId, msgId, request["read"].GetBool()); + Document response; response.SetObject(); response.AddMember("ok", ok, response.GetAllocator()); + closeJson(session, ok ? restbed::OK : restbed::NOT_FOUND, response); + })); + + resources.push_back(resource("/rsFeedReader/removeMessage", [this](auto session, const Document& request) + { + uint32_t feedId = 0; std::string msgId; + if(!uintMember(request, "feedId", feedId) || !stringMember(request, "msgId", msgId)) return closeError(session, restbed::BAD_REQUEST, "feedId and msgId are required"); + const bool ok = mFeedReader.removeMsg(feedId, msgId); + Document response; response.SetObject(); response.AddMember("ok", ok, response.GetAllocator()); + closeJson(session, ok ? restbed::OK : restbed::NOT_FOUND, response); + })); + + resources.push_back(resource("/rsFeedReader/getSettings", [this](auto session, const Document&) + { + Document response; response.SetObject(); response.AddMember("ok", true, response.GetAllocator()); + response.AddMember("storageTime", mFeedReader.getStandardStorageTime(), response.GetAllocator()); + response.AddMember("updateInterval", mFeedReader.getStandardUpdateInterval(), response.GetAllocator()); + response.AddMember("saveInBackground", mFeedReader.getSaveInBackground(), response.GetAllocator()); + closeJson(session, restbed::OK, response); + })); + + resources.push_back(resource("/rsFeedReader/setSettings", [this](auto session, const Document& request) + { + uint32_t value = 0; + if(request.HasMember("storageTime") && uintMember(request, "storageTime", value)) mFeedReader.setStandardStorageTime(value); + if(request.HasMember("updateInterval") && uintMember(request, "updateInterval", value)) mFeedReader.setStandardUpdateInterval(value); + if(request.HasMember("saveInBackground") && request["saveInBackground"].IsBool()) mFeedReader.setSaveInBackground(request["saveInBackground"].GetBool()); + Document response; response.SetObject(); response.AddMember("ok", true, response.GetAllocator()); closeJson(session, restbed::OK, response); + })); + + return resources; +} diff --git a/plugins/FeedReader/services/FeedReaderJsonApi.h b/plugins/FeedReader/services/FeedReaderJsonApi.h new file mode 100644 index 000000000..afc6b1c2c --- /dev/null +++ b/plugins/FeedReader/services/FeedReaderJsonApi.h @@ -0,0 +1,42 @@ +/******************************************************************************* + * plugins/FeedReader/services/FeedReaderJsonApi.h * + * * + * Copyright (C) 2026 RetroShare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * This program 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#pragma once + +#include +#include +#include + +#include "jsonapi/jsonapi.h" + +class RsFeedReader; + +class FeedReaderJsonApi final: public JsonApiResourceProvider +{ +public: + FeedReaderJsonApi(RsFeedReader& feedReader, RsJsonApi& jsonApi); + + std::vector> getResources() const override; + std::string getName() const override { return "FeedReader"; } + +private: + RsFeedReader& mFeedReader; + RsJsonApi& mJsonApi; +}; From 59116bee9a74cd59baa64f70e11373dd1c0261e8 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:51:15 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Removed=20FeedReader=E2=80=99s=20per-plugin?= =?UTF-8?q?=20startup=20restart.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added the incompatible-plugin GUI message. --- plugins/FeedReader/FeedReaderPlugin.cpp | 17 ++--------------- retroshare-gui/src/gui/settings/PluginsPage.cpp | 4 ++++ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/plugins/FeedReader/FeedReaderPlugin.cpp b/plugins/FeedReader/FeedReaderPlugin.cpp index bc20ccd2b..b36a3a5bb 100644 --- a/plugins/FeedReader/FeedReaderPlugin.cpp +++ b/plugins/FeedReader/FeedReaderPlugin.cpp @@ -110,21 +110,8 @@ void FeedReaderPlugin::setInterfaces(RsPlugInInterfaces &interfaces) std::cerr << "FeedReader: JSON API provider registered=" << mInterfaces.mJsonApi->hasResourceProvider(*mJsonApiProvider) << std::endl; - // retroshare-service and Android may start JSON API before plugins are - // initialized, while the desktop GUI configures and starts it afterwards. - // Restart only an already-running server. In the GUI case the provider is - // picked up by the normal, later startupWebServices() call. - if(mInterfaces.mJsonApi->isRunning()) - { - const std::error_condition restartError = - mInterfaces.mJsonApi->restart(true); - std::cerr << "FeedReader: JSON API restart result=" - << restartError.value() << " (" << restartError.message() - << ")" << std::endl; - } - else - std::cerr << "FeedReader: JSON API startup deferred to core" - << std::endl; + // The core publishes all providers together after plugin initialization. + // This avoids one burst-protected JSON API restart per plugin. } else std::cerr << "FeedReader: JSON API unavailable; routes not registered" diff --git a/retroshare-gui/src/gui/settings/PluginsPage.cpp b/retroshare-gui/src/gui/settings/PluginsPage.cpp index 63c6880d3..e0bd52680 100644 --- a/retroshare-gui/src/gui/settings/PluginsPage.cpp +++ b/retroshare-gui/src/gui/settings/PluginsPage.cpp @@ -67,6 +67,10 @@ settings::PluginsPage::PluginsPage(QWidget * parent, Qt::WindowFlags flags) short_status_string = tr("[loading problem]"); break ; + case PLUGIN_STATUS_WRONG_API: status_string = tr("Incompatible plugin API version. Rebuild or update this plugin."); + short_status_string = tr("[incompatible]"); + break ; + case PLUGIN_STATUS_MISSING_SVN: status_string = tr("No SVN number supplied. Please read plugin development manual.") ; short_status_string = tr("[loading problem]"); break ; From 774cfda33c42f52d172f102a4936b0c816036721 Mon Sep 17 00:00:00 2001 From: jolavillette Date: Tue, 11 Aug 2026 22:56:47 +0200 Subject: [PATCH 3/3] feedreader: drop the shutdown restart and the debug traces The JSON API restart in stop() cost a RESTART_BURST_PROTECTION wait (7s) on every exit and brought the HTTP server back up in the middle of the core teardown, republishing resources whose backing services were about to stop. It was not gratuitous: it made "delete mJsonApiProvider" safe, because the running restbed service holds the resources this provider returned and their handlers capture it. libretroshare now stops the JSON API at the top of rsGlobalShutDown(), before stopPlugins(), so the resources are already gone by the time stop() runs and the provider can be deleted directly. Without that companion change this commit would turn a slow shutdown into a use-after-free, so the two go together. Also replace the five std::cerr traces with one RsDbg line on success and an RsInfo when the JSON API is not available, matching how the rest of the tree reports. --- plugins/FeedReader/FeedReaderPlugin.cpp | 26 +++++++++---------- .../FeedReader/services/FeedReaderJsonApi.cpp | 2 -- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/plugins/FeedReader/FeedReaderPlugin.cpp b/plugins/FeedReader/FeedReaderPlugin.cpp index b36a3a5bb..ef1097366 100644 --- a/plugins/FeedReader/FeedReaderPlugin.cpp +++ b/plugins/FeedReader/FeedReaderPlugin.cpp @@ -32,6 +32,7 @@ #include "services/p3FeedReader.h" #include "services/FeedReaderJsonApi.h" #include +#include #include #include @@ -94,8 +95,6 @@ FeedReaderPlugin::FeedReaderPlugin() void FeedReaderPlugin::setInterfaces(RsPlugInInterfaces &interfaces) { mInterfaces = interfaces; - std::cerr << "FeedReader: JSON API interface pointer=" - << static_cast(mInterfaces.mJsonApi) << std::endl; mFeedReader = new p3FeedReader(mPlugInHandler, mInterfaces.mGxsForums, mInterfaces.mPosted); rsFeedReader = mFeedReader; @@ -107,15 +106,14 @@ void FeedReaderPlugin::setInterfaces(RsPlugInInterfaces &interfaces) { mJsonApiProvider = new FeedReaderJsonApi(*mFeedReader, *mInterfaces.mJsonApi); mInterfaces.mJsonApi->registerResourceProvider(*mJsonApiProvider); - std::cerr << "FeedReader: JSON API provider registered=" - << mInterfaces.mJsonApi->hasResourceProvider(*mJsonApiProvider) - << std::endl; - // The core publishes all providers together after plugin initialization. - // This avoids one burst-protected JSON API restart per plugin. + + // No restart here: the core publishes every provider together once all + // plugins have received their interfaces, which avoids one + // burst-protected JSON API restart per plugin. + RsDbg() << "FeedReader: JSON API routes registered."; } else - std::cerr << "FeedReader: JSON API unavailable; routes not registered" - << std::endl; + RsInfo() << "FeedReader: JSON API not available, routes not registered."; } ConfigPage *FeedReaderPlugin::qt_config_page() const @@ -145,11 +143,13 @@ void FeedReaderPlugin::stop() if(mJsonApiProvider) { if(mInterfaces.mJsonApi) - { mInterfaces.mJsonApi->unregisterResourceProvider(*mJsonApiProvider); - if(mInterfaces.mJsonApi->isRunning()) - mInterfaces.mJsonApi->restart(true); - } + + /* No restart here. The core stops the JSON API before it stops the + * plugins, so the restbed resources whose handlers capture this + * provider are already gone by now and deleting it is safe. Restarting + * would only wait out RESTART_BURST_PROTECTION and bring the server + * back up in the middle of the core teardown. */ delete mJsonApiProvider; mJsonApiProvider = NULL; } diff --git a/plugins/FeedReader/services/FeedReaderJsonApi.cpp b/plugins/FeedReader/services/FeedReaderJsonApi.cpp index bcd55f8d1..82951c707 100644 --- a/plugins/FeedReader/services/FeedReaderJsonApi.cpp +++ b/plugins/FeedReader/services/FeedReaderJsonApi.cpp @@ -20,7 +20,6 @@ #include "FeedReaderJsonApi.h" -#include #include #include @@ -179,7 +178,6 @@ FeedReaderJsonApi::FeedReaderJsonApi( std::vector> FeedReaderJsonApi::getResources() const { - std::cerr << "FeedReader: constructing JSON API resources" << std::endl; std::vector> resources; auto resource = [this](const std::string& path, auto handler) {