diff --git a/src/gxs/rsgenexchange.h b/src/gxs/rsgenexchange.h index 7390b140b..fc1208863 100644 --- a/src/gxs/rsgenexchange.h +++ b/src/gxs/rsgenexchange.h @@ -170,14 +170,14 @@ class RsGenExchange : public RsNxsObserver, public RsTickingThread, public RsGxs */ virtual void service_tick() = 0; - /*! - * - * @return handle to token service handle for making - * request to this gxs service - */ - RsTokenService* getTokenService(); - - void threadTick() override; /// @see RsTickingThread + /*! + * + * @return handle to token service handle for making + * request to this gxs service + */ + RsTokenService* getTokenService(); + + void threadTick() override; /// @see RsTickingThread /*! * Policy bit pattern portion diff --git a/src/retroshare/rsgxsifacehelper.h b/src/retroshare/rsgxsifacehelper.h index f37bbc27f..5c6357611 100644 --- a/src/retroshare/rsgxsifacehelper.h +++ b/src/retroshare/rsgxsifacehelper.h @@ -72,7 +72,7 @@ class RsGxsIfaceHelper mGxs(gxs), mTokenService(*gxs.getTokenService()), mMtx("GxsIfaceHelper") {} - ~RsGxsIfaceHelper() = default; + virtual ~RsGxsIfaceHelper() = default; #ifdef TO_REMOVE /*! @@ -527,6 +527,57 @@ class RsGxsIfaceHelper return st; } + /** + * @brief Get service statistics synchronously (blocking call) + * @param stats Output parameter for service statistics + * @param maxWait Maximum time to wait for the operation in milliseconds (default: 10000ms) + * @return true if statistics were successfully retrieved, false on timeout or error + * + * This is a convenience wrapper around the async token-based API. + * It blocks the calling thread until results are available or timeout occurs. + * Use this for simple operations like notification counting in GUI code. + */ + bool getServiceStatistics( + GxsServiceStatistic& stats, + std::chrono::milliseconds maxWait = std::chrono::milliseconds(10000)) + { + uint32_t token; + if (!requestServiceStatistic(token)) + return false; + + auto status = waitToken(token, maxWait); + if (status != RsTokenService::COMPLETE) + return false; + + return getServiceStatistic(token, stats); + } + + /** + * @brief Get group statistics synchronously (blocking call) + * @param grpId The group ID to get statistics for + * @param stats Output parameter for group statistics + * @param maxWait Maximum time to wait for the operation in milliseconds (default: 10000ms) + * @return true if statistics were successfully retrieved, false on timeout or error + * + * This is a convenience wrapper around the async token-based API. + * It blocks the calling thread until results are available or timeout occurs. + */ + bool getGroupStatistics( + const RsGxsGroupId& grpId, + GxsGroupStatistic& stats, + std::chrono::milliseconds maxWait = std::chrono::milliseconds(10000)) + { + uint32_t token; + if (!requestGroupStatistic(token, grpId)) + return false; + + auto status = waitToken(token, maxWait); + if (status != RsTokenService::COMPLETE) + return false; + + return getGroupStatistic(token, stats); + } + private: RsGxsIface& mGxs; RsTokenService& mTokenService; diff --git a/src/retroshare/rswiki.h b/src/retroshare/rswiki.h index 0a797caf7..7b3416098 100644 --- a/src/retroshare/rswiki.h +++ b/src/retroshare/rswiki.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include "retroshare/rstokenservice.h" @@ -67,8 +68,14 @@ extern RsWiki *rsWiki; /** Wiki Event Codes */ enum class RsWikiEventCode : uint8_t { - UPDATED_SNAPSHOT = 0x01, - UPDATED_COLLECTION = 0x02 + UNKNOWN = 0x00, + UPDATED_SNAPSHOT = 0x01, // Existing page modified + UPDATED_COLLECTION = 0x02, // Existing wiki group modified + NEW_SNAPSHOT = 0x03, // First-time page creation + NEW_COLLECTION = 0x04, // New wiki group creation + SUBSCRIBE_STATUS_CHANGED = 0x05, // User subscribed/unsubscribed + NEW_COMMENT = 0x06, // New comment added + READ_STATUS_CHANGED = 0x07 // Read/unread status changed }; /** Specific Wiki Event for UI updates */ @@ -80,12 +87,14 @@ struct RsGxsWikiEvent : public RsEvent RsWikiEventCode mWikiEventCode; RsGxsGroupId mWikiGroupId; + RsGxsMessageId mWikiMsgId; void serial_process(RsGenericSerializer::SerializeJob j, RsGenericSerializer::SerializeContext& ctx) override { RsEvent::serial_process(j, ctx); RS_SERIAL_PROCESS(mWikiEventCode); RS_SERIAL_PROCESS(mWikiGroupId); + RS_SERIAL_PROCESS(mWikiMsgId); } }; @@ -94,6 +103,8 @@ struct RsWikiCollection: RsGxsGenericGroupData std::string mDescription; std::string mCategory; std::string mHashTags; + // Map of moderator IDs to their termination timestamps (0 means active). + std::map mModeratorTerminationDates; }; class RsWikiSnapshot @@ -135,6 +146,82 @@ class RsWiki: public RsGxsIfaceHelper virtual bool createCollection(RsWikiCollection &collection) = 0; virtual bool updateCollection(const RsWikiCollection &collection) = 0; virtual bool getCollections(const std::list groupIds, std::vector &groups) = 0; + + /* Moderator Management */ + /** + * @brief Add a moderator to a wiki collection + * @param grpId The ID of the wiki collection/group + * @param moderatorId The ID of the user to add as moderator + * @return true if the moderator was successfully added, false otherwise + */ + virtual bool addModerator(const RsGxsGroupId& grpId, const RsGxsId& moderatorId) = 0; + + /** + * @brief Remove a moderator from a wiki collection + * @param grpId The ID of the wiki collection/group + * @param moderatorId The ID of the moderator to remove + * @return true if the moderator was successfully removed, false otherwise + */ + virtual bool removeModerator(const RsGxsGroupId& grpId, const RsGxsId& moderatorId) = 0; + + /** + * @brief Get the list of moderators for a wiki collection + * @param grpId The ID of the wiki collection/group + * @param moderators Output parameter that will contain the list of moderator IDs + * @return true if the list was successfully retrieved, false otherwise + */ + virtual bool getModerators(const RsGxsGroupId& grpId, std::list& moderators) = 0; + + /** + * @brief Check if a user is an active moderator at a given time + * @param grpId The ID of the wiki collection/group + * @param authorId The ID of the user to check + * @param editTime The time at which to check moderator status + * @return true if the user is an active moderator at the specified time, false otherwise + */ + virtual bool isActiveModerator(const RsGxsGroupId& grpId, const RsGxsId& authorId, rstime_t editTime) = 0; + + /* Content fetching for merge operations (Todo 3) */ + /** + * @brief Get the latest page content for a snapshot lineage. + * @param grpId The group ID of the wiki collection. + * @param snapshotId The message ID of the snapshot (any edit in the lineage). + * @param content Output parameter for page content + * @return true if snapshot found and content retrieved + */ + virtual bool getSnapshotContent(const RsGxsGroupId& grpId, + const RsGxsMessageId& snapshotId, + std::string& content) = 0; + + /** + * @brief Get page content from multiple snapshots efficiently (bulk fetch) + * @param grpId The group ID of the wiki collection. + * @param snapshotIds Vector of snapshot message IDs to fetch + * @param contents Output map of snapshotId -> content + * @return true if the operation completed successfully (contents may be empty) + */ + virtual bool getSnapshotsContent(const RsGxsGroupId& grpId, + const std::vector& snapshotIds, + std::map& contents) = 0; + + /* Notification support */ + /** + * @brief Get Wiki service statistics for notification counting + * @param stats Output parameter for service statistics including unread message counts + * @return true if statistics were successfully retrieved, false otherwise + * + * This method is designed for GUI notification systems to efficiently count + * new/unread messages across all Wiki collections. + */ + virtual bool getWikiStatistics(GxsServiceStatistic& stats) = 0; + + /** + * @brief Update read status for a wiki snapshot/comment + * @param token Output token for async processing + * @param msgId Group/message identifier pair to update + * @param read True to mark as read, false to mark as unread + */ + virtual void setMessageReadStatus(uint32_t& token, const RsGxsGrpMsgIdPair& msgId, bool read) = 0; }; #endif diff --git a/src/rsitems/rswikiitems.cc b/src/rsitems/rswikiitems.cc index e7ffa8508..0428e2b2e 100644 --- a/src/rsitems/rswikiitems.cc +++ b/src/rsitems/rswikiitems.cc @@ -47,6 +47,7 @@ void RsGxsWikiCollectionItem::clear() collection.mDescription.clear(); collection.mCategory.clear(); collection.mHashTags.clear(); + collection.mModeratorTerminationDates.clear(); } void RsGxsWikiCollectionItem::serial_process(RsGenericSerializer::SerializeJob j,RsGenericSerializer::SerializeContext& ctx) @@ -54,6 +55,7 @@ void RsGxsWikiCollectionItem::serial_process(RsGenericSerializer::SerializeJob j RsTypeSerializer::serial_process(j,ctx,TLV_TYPE_STR_DESCR ,collection.mDescription,"collection.mDescription") ; RsTypeSerializer::serial_process(j,ctx,TLV_TYPE_STR_CATEGORY,collection.mCategory ,"collection.mCategory") ; RsTypeSerializer::serial_process(j,ctx,TLV_TYPE_STR_HASH_TAG,collection.mHashTags ,"collection.mHashTags") ; + RsTypeSerializer::serial_process(j,ctx,collection.mModeratorTerminationDates,"collection.mModeratorTerminationDates") ; } void RsGxsWikiSnapshotItem::clear() @@ -77,4 +79,3 @@ void RsGxsWikiCommentItem::serial_process(RsGenericSerializer::SerializeJob j,Rs { RsTypeSerializer::serial_process(j,ctx,TLV_TYPE_STR_COMMENT,comment.mComment,"comment.mComment") ; } - diff --git a/src/services/p3wiki.cc b/src/services/p3wiki.cc index b00240ca0..e50bb7f3d 100644 --- a/src/services/p3wiki.cc +++ b/src/services/p3wiki.cc @@ -24,12 +24,16 @@ #include "rsitems/rswikiitems.h" #include "util/rsrandom.h" #include "retroshare/rsevents.h" +#include +#include +#include RsWiki *rsWiki = NULL; p3Wiki::p3Wiki(RsGeneralDataService* gds, RsNetworkExchangeService* nes, RsGixs *gixs) :RsGenExchange(gds, nes, new RsGxsWikiSerialiser(), RS_SERVICE_GXS_TYPE_WIKI, gixs, wikiAuthenPolicy()), - RsWiki(static_cast(*this)) + RsWiki(static_cast(*this)), + mKnownWikisMutex("GXS wiki known collections timestamp cache") { } @@ -64,13 +68,73 @@ void p3Wiki::notifyChanges(std::vector& changes) for(auto change : changes) { std::shared_ptr event = std::make_shared(wikiEventType); event->mWikiGroupId = change->mGroupId; + event->mWikiEventCode = RsWikiEventCode::UPDATED_SNAPSHOT; - if (dynamic_cast(change)) { + // Handle message changes (snapshots and comments) + RsGxsMsgChange* msgChange = dynamic_cast(change); + if (msgChange) { + // Check if this is a comment or a snapshot + if (nullptr != dynamic_cast(msgChange->mNewMsgItem)) { + // This is a comment + if (msgChange->getType() == RsGxsNotify::TYPE_RECEIVED_NEW || + msgChange->getType() == RsGxsNotify::TYPE_PUBLISHED) { + event->mWikiEventCode = RsWikiEventCode::NEW_COMMENT; + } else { + // Comments are typically not updated, but handle it as UPDATED_SNAPSHOT + event->mWikiEventCode = RsWikiEventCode::UPDATED_SNAPSHOT; + } + } else { + // This is a snapshot (page) + if (msgChange->getType() == RsGxsNotify::TYPE_RECEIVED_NEW || + msgChange->getType() == RsGxsNotify::TYPE_PUBLISHED) { + event->mWikiEventCode = RsWikiEventCode::NEW_SNAPSHOT; + } else { + event->mWikiEventCode = RsWikiEventCode::UPDATED_SNAPSHOT; + } + } + } + + // Handle message delete changes (no message data available) + RsGxsMsgDeletedChange* msgDeletedChange = dynamic_cast(change); + if (msgDeletedChange) { event->mWikiEventCode = RsWikiEventCode::UPDATED_SNAPSHOT; - } else { - // This handles new Wikis - event->mWikiEventCode = RsWikiEventCode::UPDATED_COLLECTION; } + + // Handle group changes (collections) + RsGxsGroupChange* grpChange = dynamic_cast(change); + if (grpChange) { + switch (grpChange->getType()) { + case RsGxsNotify::TYPE_PROCESSED: + // User subscribed/unsubscribed to wiki + event->mWikiEventCode = RsWikiEventCode::SUBSCRIBE_STATUS_CHANGED; + break; + + case RsGxsNotify::TYPE_RECEIVED_NEW: + case RsGxsNotify::TYPE_PUBLISHED: + { + // Check if this is a new wiki or an update + bool isNew; + { + RS_STACK_MUTEX(mKnownWikisMutex); + isNew = (mKnownWikis.find(grpChange->mGroupId) == mKnownWikis.end()); + mKnownWikis[grpChange->mGroupId] = time(NULL); + } + + if (isNew) { + event->mWikiEventCode = RsWikiEventCode::NEW_COLLECTION; + } else { + event->mWikiEventCode = RsWikiEventCode::UPDATED_COLLECTION; + } + break; + } + + default: + // For other group events, use UPDATED_COLLECTION + event->mWikiEventCode = RsWikiEventCode::UPDATED_COLLECTION; + break; + } + } + rsEvents->postEvent(event); delete change; } @@ -229,6 +293,301 @@ bool p3Wiki::getCollections(const std::list groupIds, std::vector< return getCollections(token, groups) && !groups.empty(); } +bool p3Wiki::addModerator(const RsGxsGroupId& grpId, const RsGxsId& moderatorId) +{ + std::vector collections; + if (!getCollections({grpId}, collections) || collections.empty()) + return false; + + RsWikiCollection& collection = collections.front(); + // A termination date of 0 means the moderator is active without an expiry. + collection.mModeratorTerminationDates[moderatorId] = 0; + + uint32_t token; + return updateCollection(token, collection) && waitToken(token) == RsTokenService::COMPLETE; +} + +bool p3Wiki::removeModerator(const RsGxsGroupId& grpId, const RsGxsId& moderatorId) +{ + std::vector collections; + if (!getCollections({grpId}, collections) || collections.empty()) + return false; + + RsWikiCollection& collection = collections.front(); + auto it = collection.mModeratorTerminationDates.find(moderatorId); + if (it == collection.mModeratorTerminationDates.end()) + return false; + + it->second = time(nullptr); + + uint32_t token; + return updateCollection(token, collection) && waitToken(token) == RsTokenService::COMPLETE; +} + +bool p3Wiki::getModerators(const RsGxsGroupId& grpId, std::list& moderators) +{ + std::vector collections; + if (!getCollections({grpId}, collections) || collections.empty()) + return false; + + moderators.clear(); + for (const auto& entry : collections.front().mModeratorTerminationDates) + { + // A termination date of 0 means the moderator is active without an expiry. + if (entry.second == 0) + moderators.push_back(entry.first); + } + return true; +} + +bool p3Wiki::isActiveModerator(const RsGxsGroupId& grpId, const RsGxsId& authorId, rstime_t editTime) +{ + RsWikiCollection collection; + if (!getCollectionData(grpId, collection)) + return false; + + auto it = collection.mModeratorTerminationDates.find(authorId); + if (it == collection.mModeratorTerminationDates.end()) + return false; + + // A termination date of 0 means the moderator is active without an expiry. + if (it->second == 0) + return true; + + // Reject edits made at or after the termination timestamp (termination is inclusive) + if (editTime >= it->second) + return false; + + return true; +} + +bool p3Wiki::getSnapshotContent(const RsGxsGroupId& grpId, + const RsGxsMessageId& snapshotId, + std::string& content) +{ + if (grpId.isNull() || snapshotId.isNull()) + return false; + + RsTokReqOptions metaOpts; + metaOpts.mReqType = GXS_REQUEST_TYPE_MSG_META; + GxsMsgReq msgReq; + msgReq[grpId].insert(snapshotId); + + uint32_t metaToken; + if (!requestMsgInfo(metaToken, metaOpts, msgReq) || waitToken(metaToken) != RsTokenService::COMPLETE) + { + std::cerr << "p3Wiki::getSnapshotContent() meta request failed" << std::endl; + return false; + } + + GxsMsgMetaMap metaMap; + if (!RsGenExchange::getMsgMeta(metaToken, metaMap) || metaMap[grpId].empty()) + { + std::cerr << "p3Wiki::getSnapshotContent() missing meta for snapshot: " << snapshotId << std::endl; + return false; + } + + const RsMsgMetaData& meta = metaMap[grpId].front(); + const RsGxsMessageId rootId = (!meta.mOrigMsgId.isNull()) ? meta.mOrigMsgId : snapshotId; + + RsTokReqOptions relatedOpts; + relatedOpts.mReqType = GXS_REQUEST_TYPE_MSG_RELATED_DATA; + relatedOpts.mOptions = RS_TOKREQOPT_MSG_VERSIONS; + + std::vector relatedIds; + relatedIds.emplace_back(grpId, rootId); + + uint32_t relatedToken; + if (requestMsgRelatedInfo(relatedToken, relatedOpts, relatedIds) + && waitToken(relatedToken) == RsTokenService::COMPLETE) + { + std::vector snapshots; + if (getRelatedSnapshots(relatedToken, snapshots) && !snapshots.empty()) + { + auto latestIt = std::max_element( + snapshots.begin(), snapshots.end(), + [](const RsWikiSnapshot& left, const RsWikiSnapshot& right) + { + return left.mMeta.mPublishTs < right.mMeta.mPublishTs; + }); + + content = latestIt->mPage; + return true; + } + } + + RsTokReqOptions dataOpts; + dataOpts.mReqType = GXS_REQUEST_TYPE_MSG_DATA; + + uint32_t dataToken; + if (!requestMsgInfo(dataToken, dataOpts, msgReq) || waitToken(dataToken) != RsTokenService::COMPLETE) + { + std::cerr << "p3Wiki::getSnapshotContent() data request failed" << std::endl; + return false; + } + + std::vector snapshots; + if (!getSnapshots(dataToken, snapshots) || snapshots.empty()) + { + std::cerr << "p3Wiki::getSnapshotContent() snapshot not found: " << snapshotId << std::endl; + return false; + } + + content = snapshots.front().mPage; + return true; +} + +bool p3Wiki::getSnapshotsContent(const RsGxsGroupId& grpId, + const std::vector& snapshotIds, + std::map& contents) +{ + // Allow empty input - just return success with empty map + if (snapshotIds.empty()) + return true; + + // Ensure output map does not contain stale entries from previous calls + contents.clear(); + + if (grpId.isNull()) + return false; + + RsTokReqOptions opts; + opts.mReqType = GXS_REQUEST_TYPE_MSG_DATA; + + GxsMsgReq msgReq; + std::set& requested = msgReq[grpId]; + requested.insert(snapshotIds.begin(), snapshotIds.end()); + + uint32_t token; + if (!requestMsgInfo(token, opts, msgReq)) + { + std::cerr << "p3Wiki::getSnapshotsContent() requestMsgInfo failed" << std::endl; + return false; + } + + if (waitToken(token) != RsTokenService::COMPLETE) + { + std::cerr << "p3Wiki::getSnapshotsContent() request failed" << std::endl; + return false; + } + + std::vector snapshots; + if (!getSnapshots(token, snapshots)) + { + std::cerr << "p3Wiki::getSnapshotsContent() failed to get snapshots" << std::endl; + return false; + } + + for (const auto& snapshot : snapshots) + { + contents[snapshot.mMeta.mMsgId] = snapshot.mPage; + } + + return true; +} + +bool p3Wiki::acceptNewMessage(const RsGxsMsgMetaData *msgMeta, uint32_t /*size*/) +{ + if (!msgMeta) + return false; + + if (msgMeta->mOrigMsgId.isNull() || msgMeta->mOrigMsgId == msgMeta->mMsgId) + return true; + + RsGxsId originalAuthorId; + if (!getOriginalMessageAuthor(msgMeta->mGroupId, msgMeta->mOrigMsgId, originalAuthorId)) + { + std::cerr << "p3Wiki: Rejecting edit " << msgMeta->mMsgId + << " in group " << msgMeta->mGroupId + << " without original author data." << std::endl; + return false; + } + + if (msgMeta->mAuthorId == originalAuthorId) + return true; + + if (!checkModeratorPermission(msgMeta->mGroupId, msgMeta->mAuthorId, originalAuthorId, msgMeta->mPublishTs)) + { + std::cerr << "p3Wiki: Rejecting edit from non-moderator " << msgMeta->mAuthorId + << " in group " << msgMeta->mGroupId + << " on message by " << originalAuthorId << std::endl; + return false; + } + + return true; +} + +bool p3Wiki::checkModeratorPermission(const RsGxsGroupId& grpId, const RsGxsId& authorId, const RsGxsId& originalAuthorId, rstime_t editTime) +{ + return isActiveModerator(grpId, authorId, editTime); +} + +bool p3Wiki::getCollectionData(const RsGxsGroupId& grpId, RsWikiCollection& collection) +{ + std::vector collections; + if (!getCollections({grpId}, collections) || collections.empty()) + return false; + + collection = collections.front(); + return true; +} + +bool p3Wiki::getOriginalMessageAuthor(const RsGxsGroupId& grpId, const RsGxsMessageId& msgId, RsGxsId& authorId) +{ + GxsMsgReq req; + req[grpId].insert(msgId); + + RsTokReqOptions opts; + opts.mReqType = GXS_REQUEST_TYPE_MSG_META; + + uint32_t token; + if (!requestMsgInfo(token, opts, req) || waitToken(token) != RsTokenService::COMPLETE) + return false; + + GxsMsgMetaMap metaMap; + if (!RsGenExchange::getMsgMeta(token, metaMap)) + return false; + + auto groupIt = metaMap.find(grpId); + if (groupIt == metaMap.end()) + return false; + + for (const auto& meta : groupIt->second) + { + if (meta.mMsgId == msgId) + { + authorId = meta.mAuthorId; + return true; + } + } + + return false; +} + +bool p3Wiki::getWikiStatistics(GxsServiceStatistic& stats) +{ + // Use the protected blocking helper from RsGxsIfaceHelper + return getServiceStatistics(stats); +} + +void p3Wiki::setMessageReadStatus(uint32_t& token, const RsGxsGrpMsgIdPair& msgId, bool read) +{ + const uint32_t mask = GXS_SERV::GXS_MSG_STATUS_GUI_NEW | GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD; + const uint32_t status = read ? 0 : GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD; + + setMsgStatusFlags(token, msgId, status, mask); + + if (rsEvents) + { + RsEventType wikiEventType = rsEvents->getDynamicEventType("GXS_WIKI"); + auto event = std::make_shared(wikiEventType); + event->mWikiEventCode = RsWikiEventCode::READ_STATUS_CHANGED; + event->mWikiGroupId = msgId.first; + event->mWikiMsgId = msgId.second; + rsEvents->postEvent(event); + } +} + /* Stream operators for debugging */ std::ostream &operator<<(std::ostream &out, const RsWikiCollection &group) @@ -247,4 +606,4 @@ std::ostream &operator<<(std::ostream &out, const RsWikiComment &comment) { out << "RsWikiComment [ Title: " << comment.mMeta.mMsgName << "]"; return out; -} \ No newline at end of file +} diff --git a/src/services/p3wiki.h b/src/services/p3wiki.h index 0325bc71d..25c534652 100644 --- a/src/services/p3wiki.h +++ b/src/services/p3wiki.h @@ -66,6 +66,36 @@ class p3Wiki: public RsGenExchange, public RsWiki virtual bool createCollection(RsWikiCollection &collection) override; virtual bool updateCollection(const RsWikiCollection &collection) override; virtual bool getCollections(const std::list groupIds, std::vector &groups) override; + + /* Moderator management */ + virtual bool addModerator(const RsGxsGroupId& grpId, const RsGxsId& moderatorId) override; + virtual bool removeModerator(const RsGxsGroupId& grpId, const RsGxsId& moderatorId) override; + virtual bool getModerators(const RsGxsGroupId& grpId, std::list& moderators) override; + virtual bool isActiveModerator(const RsGxsGroupId& grpId, const RsGxsId& authorId, rstime_t editTime) override; + + /* Content fetching for merge operations (Todo 3) */ + virtual bool getSnapshotContent(const RsGxsGroupId& grpId, + const RsGxsMessageId& snapshotId, + std::string& content) override; + virtual bool getSnapshotsContent(const RsGxsGroupId& grpId, + const std::vector& snapshotIds, + std::map& contents) override; + + /* Notification support */ + virtual bool getWikiStatistics(GxsServiceStatistic& stats) override; + virtual void setMessageReadStatus(uint32_t& token, const RsGxsGrpMsgIdPair& msgId, bool read) override; + +protected: + bool acceptNewMessage(const RsGxsMsgMetaData *msgMeta, uint32_t size) override; + +private: + bool checkModeratorPermission(const RsGxsGroupId& grpId, const RsGxsId& authorId, const RsGxsId& originalAuthorId, rstime_t editTime); + bool getCollectionData(const RsGxsGroupId& grpId, RsWikiCollection& collection); + bool getOriginalMessageAuthor(const RsGxsGroupId& grpId, const RsGxsMessageId& msgId, RsGxsId& authorId); + + // Track known wikis to distinguish NEW from UPDATED + std::map mKnownWikis; + RsMutex mKnownWikisMutex; }; #endif