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..ef1097366 100644 --- a/plugins/FeedReader/FeedReaderPlugin.cpp +++ b/plugins/FeedReader/FeedReaderPlugin.cpp @@ -30,6 +30,9 @@ #include "gui/FeedReaderConfig.h" #include "gui/FeedReaderFeedNotify.h" #include "services/p3FeedReader.h" +#include "services/FeedReaderJsonApi.h" +#include +#include #include #include @@ -83,6 +86,7 @@ FeedReaderPlugin::FeedReaderPlugin() mFeedReader = NULL; mNotify = NULL; mFeedNotify = NULL; + mJsonApiProvider = NULL; Q_INIT_RESOURCE(FeedReader_images); Q_INIT_RESOURCE(FeedReader_qss); @@ -97,6 +101,19 @@ void FeedReaderPlugin::setInterfaces(RsPlugInInterfaces &interfaces) mNotify = new FeedReaderNotify(); mFeedReader->setNotify(mNotify); + + if(mInterfaces.mJsonApi) + { + mJsonApiProvider = new FeedReaderJsonApi(*mFeedReader, *mInterfaces.mJsonApi); + mInterfaces.mJsonApi->registerResourceProvider(*mJsonApiProvider); + + // 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 + RsInfo() << "FeedReader: JSON API not available, routes not registered."; } ConfigPage *FeedReaderPlugin::qt_config_page() const @@ -123,6 +140,19 @@ FeedNotify *FeedReaderPlugin::qt_feedNotify() void FeedReaderPlugin::stop() { + if(mJsonApiProvider) + { + if(mInterfaces.mJsonApi) + mInterfaces.mJsonApi->unregisterResourceProvider(*mJsonApiProvider); + + /* 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; + } 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..82951c707 --- /dev/null +++ b/plugins/FeedReader/services/FeedReaderJsonApi.cpp @@ -0,0 +1,337 @@ +/******************************************************************************* + * 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 "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::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; +}; 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 ;