OpChan/src/lib/forum/transformers.ts
2025-09-10 17:28:03 +05:30

311 lines
9.7 KiB
TypeScript

import { Cell, Post, Comment } from '@/types/forum';
import {
CellMessage,
CommentMessage,
PostMessage,
VoteMessage,
EModerationAction,
} from '@/types/waku';
import messageManager from '@/lib/waku';
import { RelevanceCalculator } from './RelevanceCalculator';
import { UserVerificationStatus } from '@/types/forum';
// Validation is enforced at ingestion time by LocalDatabase. Transformers assume
// cache contains only valid, verified messages.
export const transformCell = async (
cellMessage: CellMessage,
_verifyMessage?: unknown,
userVerificationStatus?: UserVerificationStatus,
posts?: Post[]
): Promise<Cell | null> => {
// 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,
posts
);
// Calculate active member count
const cellPosts = posts.filter(post => post.cellId === cellMessage.id);
const activeMembers = new Set<string>();
cellPosts.forEach(post => {
activeMembers.add(post.authorAddress);
});
return {
...transformedCell,
relevanceScore: relevanceResult.score,
activeMemberCount: activeMembers.size,
relevanceDetails: relevanceResult.details,
};
}
return transformedCell;
};
export const transformPost = async (
postMessage: PostMessage,
_verifyMessage?: unknown,
userVerificationStatus?: UserVerificationStatus
): Promise<Post | null> => {
// Message validity already enforced upstream
const votes = Object.values(messageManager.messageCache.votes).filter(
vote => vote.targetId === postMessage.id
);
// Votes in cache are already validated; just map
const filteredVotes = votes;
const upvotes = filteredVotes.filter(
(vote): vote is VoteMessage => vote !== null && vote.value === 1
);
const downvotes = filteredVotes.filter(
(vote): vote is VoteMessage => vote !== null && vote.value === -1
);
const modMsg = messageManager.messageCache.moderations[postMessage.id];
const isPostModerated =
!!modMsg &&
modMsg.targetType === 'post' &&
modMsg.action === EModerationAction.MODERATE;
const userModMsg = Object.values(
messageManager.messageCache.moderations
).find(
m =>
m.targetType === 'user' &&
m.cellId === postMessage.cellId &&
m.targetId === postMessage.author
);
const isUserModerated =
!!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,
upvotes,
downvotes,
moderated: isPostModerated || isUserModerated,
moderatedBy: isPostModerated
? modMsg.author
: isUserModerated
? userModMsg!.author
: undefined,
moderationReason: isPostModerated
? modMsg.reason
: isUserModerated
? userModMsg!.reason
: undefined,
moderationTimestamp: isPostModerated
? modMsg.timestamp
: isUserModerated
? userModMsg!.timestamp
: undefined,
// mark pending for optimistic UI if not yet acknowledged
// not persisted as a field; UI can check via LocalDatabase
};
// Calculate relevance score if user verification status is provided
if (userVerificationStatus) {
const relevanceCalculator = new RelevanceCalculator();
// Get comments for this post
const comments = await Promise.all(
Object.values(messageManager.messageCache.comments).map(comment =>
transformComment(comment, undefined, userVerificationStatus)
)
).then(comments =>
comments.filter((comment): comment is Comment => comment !== null)
);
const postComments = comments.filter(
comment => comment.postId === postMessage.id
);
const relevanceResult = relevanceCalculator.calculatePostScore(
transformedPost,
filteredVotes,
postComments,
userVerificationStatus
);
const relevanceScore = relevanceResult.score;
// Calculate verified upvotes and commenters
const verifiedUpvotes = upvotes.filter(vote => {
const voterStatus = userVerificationStatus[vote.author];
return voterStatus?.isVerified;
}).length;
const verifiedCommenters = new Set<string>();
postComments.forEach(comment => {
const commenterStatus = userVerificationStatus[comment.authorAddress];
if (commenterStatus?.isVerified) {
verifiedCommenters.add(comment.authorAddress);
}
});
return {
...transformedPost,
relevanceScore,
verifiedUpvotes,
verifiedCommenters: Array.from(verifiedCommenters),
relevanceDetails: relevanceResult.details,
};
}
return transformedPost;
};
export const transformComment = async (
commentMessage: CommentMessage,
_verifyMessage?: unknown,
userVerificationStatus?: UserVerificationStatus
): Promise<Comment | null> => {
// Message validity already enforced upstream
const votes = Object.values(messageManager.messageCache.votes).filter(
vote => vote.targetId === commentMessage.id
);
// Votes in cache are already validated
const filteredVotes = votes;
const upvotes = filteredVotes.filter(
(vote): vote is VoteMessage => vote !== null && vote.value === 1
);
const downvotes = filteredVotes.filter(
(vote): vote is VoteMessage => vote !== null && vote.value === -1
);
const modMsg = messageManager.messageCache.moderations[commentMessage.id];
const isCommentModerated =
!!modMsg &&
modMsg.targetType === 'comment' &&
modMsg.action === EModerationAction.MODERATE;
// Find the post to get the correct cell ID
const parentPost = Object.values(messageManager.messageCache.posts).find(
post => post.id === commentMessage.postId
);
const userModMsg = Object.values(
messageManager.messageCache.moderations
).find(
m =>
m.targetType === 'user' &&
m.cellId === parentPost?.cellId &&
m.targetId === commentMessage.author
);
const isUserModerated =
!!userModMsg && userModMsg.action === EModerationAction.MODERATE;
const transformedComment: Comment = {
id: commentMessage.id,
type: commentMessage.type,
author: commentMessage.author,
postId: commentMessage.postId,
authorAddress: commentMessage.author,
content: commentMessage.content,
timestamp: commentMessage.timestamp,
signature: commentMessage.signature,
browserPubKey: commentMessage.browserPubKey,
delegationProof: commentMessage.delegationProof,
upvotes,
downvotes,
moderated: isCommentModerated || isUserModerated,
moderatedBy: isCommentModerated
? modMsg.author
: isUserModerated
? userModMsg!.author
: undefined,
moderationReason: isCommentModerated
? modMsg.reason
: isUserModerated
? userModMsg!.reason
: undefined,
moderationTimestamp: isCommentModerated
? modMsg.timestamp
: isUserModerated
? userModMsg!.timestamp
: undefined,
// mark pending for optimistic UI via LocalDatabase lookup
};
// Calculate relevance score if user verification status is provided
if (userVerificationStatus) {
const relevanceCalculator = new RelevanceCalculator();
const relevanceResult = relevanceCalculator.calculateCommentScore(
transformedComment,
filteredVotes.filter((vote): vote is VoteMessage => vote !== null),
userVerificationStatus
);
return {
...transformedComment,
relevanceScore: relevanceResult.score,
relevanceDetails: relevanceResult.details,
};
}
return transformedComment;
};
export const transformVote = async (
voteMessage: VoteMessage,
_verifyMessage?: unknown
): Promise<VoteMessage | null> => {
// Message validity already enforced upstream
return voteMessage;
};
export const getDataFromCache = async (
_verifyMessage?: unknown, // Deprecated parameter, kept for compatibility
userVerificationStatus?: UserVerificationStatus
): Promise<{ cells: Cell[]; posts: Post[]; comments: Comment[] }> => {
// First transform posts and comments to get relevance scores
// All validation is now handled internally by the transform functions
const posts = await Promise.all(
Object.values(messageManager.messageCache.posts).map(post =>
transformPost(post, undefined, userVerificationStatus)
)
).then(posts => posts.filter((post): post is Post => post !== null));
const comments = await Promise.all(
Object.values(messageManager.messageCache.comments).map(c =>
transformComment(c, undefined, userVerificationStatus)
)
).then(comments =>
comments.filter((comment): comment is Comment => comment !== null)
);
// Then transform cells with posts for relevance calculation
const cells = await Promise.all(
Object.values(messageManager.messageCache.cells).map(cell =>
transformCell(cell, undefined, userVerificationStatus, posts)
)
).then(cells => cells.filter((cell): cell is Cell => cell !== null));
return { cells, posts, comments };
};