/******************************************************************************* * 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; }