From 3d3eafd626c0e29021d3e540e648dbad67cc6d47 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Fri, 3 Oct 2025 19:06:11 +0530 Subject: [PATCH] chore: user cannot moderate themselves --- app/src/components/CommentCard.tsx | 4 +- .../core/src/lib/database/LocalDatabase.ts | 33 +++++++++----- packages/core/src/lib/database/schema.ts | 10 +++-- packages/core/src/lib/forum/ForumActions.ts | 27 ++++++++++++ packages/core/src/lib/forum/transformers.ts | 43 +++---------------- packages/react/src/v1/hooks/useContent.ts | 6 ++- 6 files changed, 66 insertions(+), 57 deletions(-) diff --git a/app/src/components/CommentCard.tsx b/app/src/components/CommentCard.tsx index 1ba62c8..304c129 100644 --- a/app/src/components/CommentCard.tsx +++ b/app/src/components/CommentCard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ArrowUp, ArrowDown, Clock, Shield, UserX } from 'lucide-react'; +import { ArrowUp, ArrowDown, Clock, MessageSquareX, UserX } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; import type { Comment } from '@opchan/core'; import { Button } from '@/components/ui/button'; @@ -174,7 +174,7 @@ const CommentCard: React.FC = ({ className="h-6 w-6 text-cyber-neutral hover:text-orange-500" onClick={() => onModerateComment(comment.id)} > - + diff --git a/packages/core/src/lib/database/LocalDatabase.ts b/packages/core/src/lib/database/LocalDatabase.ts index 0016070..6fe73d5 100644 --- a/packages/core/src/lib/database/LocalDatabase.ts +++ b/packages/core/src/lib/database/LocalDatabase.ts @@ -24,7 +24,11 @@ export interface LocalDatabaseCache { posts: PostCache; comments: CommentCache; votes: VoteCache; - moderations: { [targetId: string]: ModerateMessage }; + // Moderations keyed by composite key: + // - post: 'post:' + // - comment: 'comment:' + // - user: ':user:
' + moderations: { [key: string]: (ModerateMessage & { key?: string }) }; userIdentities: UserIdentityCache; bookmarks: BookmarkCache; } @@ -212,13 +216,20 @@ export class LocalDatabase { } case MessageType.MODERATE: { const modMsg = message as ModerateMessage; - if ( - !this.cache.moderations[modMsg.targetId] || - this.cache.moderations[modMsg.targetId]?.timestamp !== - modMsg.timestamp - ) { - this.cache.moderations[modMsg.targetId] = modMsg; - this.put(STORE.MODERATIONS, modMsg); + // Compose key: + // - post: `post:${postId}` + // - comment: `comment:${commentId}` + // - user: `${cellId}:user:${address}` (per-cell user moderation) + const key = + modMsg.targetType === 'user' + ? `${modMsg.cellId}:user:${modMsg.targetId}` + : `${modMsg.targetType}:${modMsg.targetId}`; + + const existing = this.cache.moderations[key]; + if (!existing || modMsg.timestamp > existing.timestamp) { + // Store in cache and persist with computed key + this.cache.moderations[key] = { ...(modMsg as ModerateMessage), key }; + this.put(STORE.MODERATIONS, { ...(modMsg as ModerateMessage), key }); } break; } @@ -267,7 +278,7 @@ export class LocalDatabase { PostMessage[], CommentMessage[], (VoteMessage & { key: string })[], - ModerateMessage[], + (ModerateMessage & { key: string })[], ({ address: string } & UserIdentityCache[string])[], Bookmark[], ] = await Promise.all([ @@ -275,7 +286,7 @@ export class LocalDatabase { this.getAllFromStore(STORE.POSTS), this.getAllFromStore(STORE.COMMENTS), this.getAllFromStore(STORE.VOTES), - this.getAllFromStore(STORE.MODERATIONS), + this.getAllFromStore(STORE.MODERATIONS), this.getAllFromStore<{ address: string } & UserIdentityCache[string]>( STORE.USER_IDENTITIES ), @@ -293,7 +304,7 @@ export class LocalDatabase { }) ); this.cache.moderations = Object.fromEntries( - moderations.map(m => [m.targetId, m]) + moderations.map(m => [m.key, m]) ); this.cache.userIdentities = Object.fromEntries( identities.map(u => { diff --git a/packages/core/src/lib/database/schema.ts b/packages/core/src/lib/database/schema.ts index 77ee96c..4a5fa2f 100644 --- a/packages/core/src/lib/database/schema.ts +++ b/packages/core/src/lib/database/schema.ts @@ -1,5 +1,5 @@ export const DB_NAME = 'opchan-local'; -export const DB_VERSION = 4; +export const DB_VERSION = 5; export const STORE = { CELLS: 'cells', @@ -49,10 +49,12 @@ export function openLocalDB(): Promise { // Votes are keyed by composite key `${targetId}:${author}` db.createObjectStore(STORE.VOTES, { keyPath: 'key' }); } - if (!db.objectStoreNames.contains(STORE.MODERATIONS)) { - // Moderations keyed by targetId - db.createObjectStore(STORE.MODERATIONS, { keyPath: 'targetId' }); + // Moderations store: recreate with composite key support + if (db.objectStoreNames.contains(STORE.MODERATIONS)) { + db.deleteObjectStore(STORE.MODERATIONS); } + // Moderations keyed by computed 'key' (e.g., 'post:postId', 'comment:commentId', 'cellId:user:userAddress') + db.createObjectStore(STORE.MODERATIONS, { keyPath: 'key' }); if (!db.objectStoreNames.contains(STORE.USER_IDENTITIES)) { // User identities keyed by address db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' }); diff --git a/packages/core/src/lib/forum/ForumActions.ts b/packages/core/src/lib/forum/ForumActions.ts index 254fabf..e7277e3 100644 --- a/packages/core/src/lib/forum/ForumActions.ts +++ b/packages/core/src/lib/forum/ForumActions.ts @@ -431,6 +431,7 @@ export class ForumActions { currentUser, isAuthenticated, cellOwner, + commentAuthor, } = params; if (!isAuthenticated || !currentUser) { @@ -446,6 +447,12 @@ export class ForumActions { error: 'Not authorized. Only the cell admin can moderate comments.', }; } + if (currentUser.address === commentAuthor) { + return { + success: false, + error: 'You cannot moderate your own comments.', + }; + } try { const unsignedMod: UnsignedModerateMessage = { @@ -520,6 +527,12 @@ export class ForumActions { error: 'Not authorized. Only the cell admin can moderate users.', }; } + if (currentUser.address === userAddress) { + return { + success: false, + error: 'You cannot moderate yourself.', + }; + } try { const unsignedMod: UnsignedModerateMessage = { @@ -647,6 +660,7 @@ export class ForumActions { currentUser, isAuthenticated, cellOwner, + commentAuthor, } = params; if (!isAuthenticated || !currentUser) { @@ -662,6 +676,12 @@ export class ForumActions { error: 'Not authorized. Only the cell admin can unmoderate comments.', }; } + if (currentUser.address === commentAuthor) { + return { + success: false, + error: 'You cannot unmoderate your own comments.', + }; + } try { const unsignedMod: UnsignedModerateMessage = { @@ -736,6 +756,12 @@ export class ForumActions { error: 'Not authorized. Only the cell admin can unmoderate users.', }; } + if (currentUser.address === userAddress) { + return { + success: false, + error: 'You cannot unmoderate yourself.', + }; + } try { const unsignedMod: UnsignedModerateMessage = { @@ -826,6 +852,7 @@ interface CommentModerationParams extends BaseActionParams { commentId: string; reason?: string; cellOwner: string; + commentAuthor: string; } interface UserModerationParams extends BaseActionParams { diff --git a/packages/core/src/lib/forum/transformers.ts b/packages/core/src/lib/forum/transformers.ts index d43c8f1..c48a82b 100644 --- a/packages/core/src/lib/forum/transformers.ts +++ b/packages/core/src/lib/forum/transformers.ts @@ -18,27 +18,11 @@ export const transformCell = async ( userVerificationStatus?: UserVerificationStatus, posts?: Post[] ): Promise => { - // Message validity already enforced upstream - - const transformedCell: Cell = { - id: cellMessage.id, - type: cellMessage.type, - author: cellMessage.author, - name: cellMessage.name, - description: cellMessage.description, - icon: cellMessage.icon || '', - timestamp: cellMessage.timestamp, - signature: cellMessage.signature, - browserPubKey: cellMessage.browserPubKey, - delegationProof: cellMessage.delegationProof, - }; - - // Calculate relevance score if user verification status and posts are provided if (userVerificationStatus && posts) { const relevanceCalculator = new RelevanceCalculator(); const relevanceResult = relevanceCalculator.calculateCellScore( - transformedCell, + cellMessage, posts ); @@ -50,14 +34,14 @@ export const transformCell = async ( }); return { - ...transformedCell, + ...cellMessage, relevanceScore: relevanceResult.score, activeMemberCount: activeMembers.size, relevanceDetails: relevanceResult.details, }; } - return transformedCell; + return cellMessage; }; export const transformPost = async ( @@ -97,17 +81,8 @@ export const transformPost = async ( !!userModMsg && userModMsg.action === EModerationAction.MODERATE; const transformedPost: Post = { - id: postMessage.id, - type: postMessage.type, - author: postMessage.author, - cellId: postMessage.cellId, authorAddress: postMessage.author, - title: postMessage.title, - content: postMessage.content, - timestamp: postMessage.timestamp, - signature: postMessage.signature, - browserPubKey: postMessage.browserPubKey, - delegationProof: postMessage.delegationProof, + ...postMessage, upvotes, downvotes, moderated: isPostModerated || isUserModerated, @@ -223,16 +198,8 @@ export const transformComment = async ( !!userModMsg && userModMsg.action === EModerationAction.MODERATE; const transformedComment: Comment = { - id: commentMessage.id, - type: commentMessage.type, - author: commentMessage.author, - postId: commentMessage.postId, + ...commentMessage, authorAddress: commentMessage.author, - content: commentMessage.content, - timestamp: commentMessage.timestamp, - signature: commentMessage.signature, - browserPubKey: commentMessage.browserPubKey, - delegationProof: commentMessage.delegationProof, upvotes, downvotes, moderated: isCommentModerated || isUserModerated, diff --git a/packages/react/src/v1/hooks/useContent.ts b/packages/react/src/v1/hooks/useContent.ts index 791e42c..ee08372 100644 --- a/packages/react/src/v1/hooks/useContent.ts +++ b/packages/react/src/v1/hooks/useContent.ts @@ -193,8 +193,9 @@ export function useContent() { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const cell = content.cells.find(c => c.id === cellId); + const comment = content.comments.find(c => c.id === commentId); const res = await client.forumActions.moderateComment( - { cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' }, + { cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '', commentAuthor: comment?.author ?? '' }, () => reflectCache(client) ); reflectCache(client); @@ -204,8 +205,9 @@ export function useContent() { const currentUser = session.currentUser; const isAuthenticated = Boolean(currentUser); const cell = content.cells.find(c => c.id === cellId); + const comment = content.comments.find(c => c.id === commentId); const res = await client.forumActions.unmoderateComment( - { cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' }, + { cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '', commentAuthor: comment?.author ?? '' }, () => reflectCache(client) ); reflectCache(client);