diff --git a/src/components/groups/PendingRepliesList.tsx b/src/components/groups/PendingRepliesList.tsx index 353023e2..16302c56 100644 --- a/src/components/groups/PendingRepliesList.tsx +++ b/src/components/groups/PendingRepliesList.tsx @@ -14,6 +14,7 @@ import { CheckCircle, AlertCircle, MessageSquare, ArrowUpRight } from "lucide-re import { toast } from "sonner"; import { NostrEvent } from "@nostrify/nostrify"; import { KINDS } from "@/lib/nostr-kinds"; +import { filterSpamPosts } from "@/lib/spam-filter"; interface PendingRepliesListProps { communityId: string; @@ -39,7 +40,10 @@ export function PendingRepliesList({ communityId }: PendingRepliesListProps) { ); } - if (!pendingReplies || pendingReplies.length === 0) { + // Filter out spam replies + const filteredPendingReplies = pendingReplies ? filterSpamPosts(pendingReplies) : []; + + if (!filteredPendingReplies || filteredPendingReplies.length === 0) { return ( @@ -56,7 +60,7 @@ export function PendingRepliesList({ communityId }: PendingRepliesListProps) {

Pending Replies

- {pendingReplies.length} pending + {filteredPendingReplies.length} pending
@@ -70,7 +74,7 @@ export function PendingRepliesList({ communityId }: PendingRepliesListProps) {
- {pendingReplies.map((reply) => ( + {filteredPendingReplies.map((reply) => ( post !== null); + // Filter out spam posts + const filteredApprovedPosts = filterSpamPosts(approvedPosts); + // Debug logging console.log("Filtered approved posts:", { - totalApprovedPosts: approvedPosts.length + totalApprovedPosts: approvedPosts.length, + afterSpamFilter: filteredApprovedPosts.length, + spamPostsRemoved: approvedPosts.length - filteredApprovedPosts.length }); - return approvedPosts; + return filteredApprovedPosts; }, enabled: !!nostr && !!communityId, }); @@ -198,14 +204,19 @@ export function PostList({ communityId, showOnlyApproved = false, pendingOnly = return replyTags.length === 0; }); + // Filter out spam posts + const spamFilteredPosts = filterSpamPosts(filteredPosts); + // Debug logging console.log("Filtered posts:", { totalPosts: posts.length, filteredPosts: filteredPosts.length, - removedReplies: posts.length - filteredPosts.length + removedReplies: posts.length - filteredPosts.length, + afterSpamFilter: spamFilteredPosts.length, + spamPostsRemoved: filteredPosts.length - spamFilteredPosts.length }); - return filteredPosts; + return spamFilteredPosts; }, enabled: !!nostr && !!communityId, }); @@ -246,7 +257,8 @@ export function PostList({ communityId, showOnlyApproved = false, pendingOnly = ids: pinnedPostIds, }], { signal }); - return posts; + // Filter out spam posts from pinned posts + return filterSpamPosts(posts); }, enabled: !!nostr && !!communityId, // Ensure the query refetches when pinnedPostIds changes diff --git a/src/components/groups/ReplyList.tsx b/src/components/groups/ReplyList.tsx index 4e475597..715d1fd7 100644 --- a/src/components/groups/ReplyList.tsx +++ b/src/components/groups/ReplyList.tsx @@ -30,6 +30,7 @@ import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import { KINDS } from "@/lib/nostr-kinds"; +import { filterSpamPosts } from "@/lib/spam-filter"; import { Tooltip, TooltipContent, @@ -88,8 +89,11 @@ export function ReplyList({ postId, communityId, postAuthorPubkey }: ReplyListPr ); } + // Filter out spam replies first + const nonSpamReplies = filterSpamPosts(replies); + // Process replies to mark which ones are approved - const processedReplies = replies.map(reply => { + const processedReplies = nonSpamReplies.map(reply => { // Check if reply is explicitly approved by a moderator const isExplicitlyApproved = isReplyApproved(reply.id); @@ -265,8 +269,11 @@ function ReplyItem({ reply, communityId, postId, postAuthorPubkey, onReplySubmit onReplySubmitted(); }; + // Filter out spam nested replies first + const nonSpamNestedReplies = nestedReplies ? filterSpamPosts(nestedReplies) : []; + // Process nested replies to mark which ones are approved - const processedNestedReplies = nestedReplies?.map(nestedReply => { + const processedNestedReplies = nonSpamNestedReplies.map(nestedReply => { // Check if reply is explicitly approved by a moderator const isExplicitlyApproved = isReplyApproved(nestedReply.id); @@ -282,7 +289,7 @@ function ReplyItem({ reply, communityId, postId, postAuthorPubkey, onReplySubmit isAutoApproved: isAuthorApproved && !isExplicitlyApproved, isPendingApproval: !isApproved }; - }) || []; + }); // Filter nested replies based on approval status if not a moderator const filteredNestedReplies = !isUserModerator diff --git a/src/lib/spam-filter.ts b/src/lib/spam-filter.ts new file mode 100644 index 00000000..bf11bca9 --- /dev/null +++ b/src/lib/spam-filter.ts @@ -0,0 +1,86 @@ +/** + * Spam filtering utilities for Chorus + * + * This module provides functions to detect and filter spam content + * based on hardcoded keywords and patterns. + */ + +// Hardcoded spam keywords - these can be easily modified as needed +const SPAM_KEYWORDS = [ + "Has nostr figured out spam yet?", + // Add more spam keywords here as needed + // "another spam phrase", + // "yet another spam phrase", +]; + +/** + * Check if text contains any spam keywords + * @param text - The text to check for spam + * @returns true if spam is detected, false otherwise + */ +export function containsSpam(text: string): boolean { + if (!text || typeof text !== 'string') { + return false; + } + + const lowerText = text.toLowerCase(); + + return SPAM_KEYWORDS.some(keyword => + lowerText.includes(keyword.toLowerCase()) + ); +} + +/** + * Check if a group should be filtered based on its name or description + * @param groupName - The group name to check + * @param groupDescription - The group description to check (optional) + * @returns true if the group should be filtered, false otherwise + */ +export function isSpamGroup(groupName: string, groupDescription?: string): boolean { + // Check group name for spam + if (containsSpam(groupName)) { + return true; + } + + // Check group description for spam if provided + if (groupDescription && containsSpam(groupDescription)) { + return true; + } + + return false; +} + +/** + * Check if a post should be filtered based on its content + * @param postContent - The post content to check + * @returns true if the post should be filtered, false otherwise + */ +export function isSpamPost(postContent: string): boolean { + return containsSpam(postContent); +} + +/** + * Filter an array of groups to remove spam groups + * @param groups - Array of group objects with name and description tags + * @returns Filtered array with spam groups removed + */ +export function filterSpamGroups> }>(groups: T[]): T[] { + return groups.filter(group => { + const nameTag = group.tags.find(tag => tag[0] === "name"); + const descriptionTag = group.tags.find(tag => tag[0] === "description"); + + const name = nameTag ? nameTag[1] : ""; + const description = descriptionTag ? descriptionTag[1] : ""; + + return !isSpamGroup(name, description); + }); +} + +/** + * Filter an array of posts to remove spam posts + * @param posts - Array of post objects with content property + * @returns Filtered array with spam posts removed + */ +export function filterSpamPosts(posts: T[]): T[] { + return posts.filter(post => !isSpamPost(post.content)); +} \ No newline at end of file diff --git a/src/pages/Groups.tsx b/src/pages/Groups.tsx index 329db554..7451cca4 100644 --- a/src/pages/Groups.tsx +++ b/src/pages/Groups.tsx @@ -20,6 +20,7 @@ import type { UserRole } from "@/hooks/useUserRole"; import { KINDS } from "@/lib/nostr-kinds"; import { useCashuWallet } from "@/hooks/useCashuWallet"; import { useGroupDeletionRequests } from "@/hooks/useGroupDeletionRequests"; +import { filterSpamGroups } from "@/lib/spam-filter"; // Helper function to get community ID const getCommunityId = (community: NostrEvent) => { @@ -70,7 +71,10 @@ export default function Groups() { ); // Ensure we always return an array, even if the query fails - return Array.isArray(events) ? events : []; + const validEvents = Array.isArray(events) ? events : []; + + // Filter out spam groups + return filterSpamGroups(validEvents); } catch (error) { console.error("Error fetching communities:", error); // Return empty array on error instead of throwing