Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/components/groups/PendingRepliesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 (
<Alert className="bg-green-50 border-green-200 text-green-800 dark:bg-green-950 dark:border-green-800 dark:text-green-200">
<CheckCircle className="h-4 w-4" />
Expand All @@ -56,7 +60,7 @@ export function PendingRepliesList({ communityId }: PendingRepliesListProps) {
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold">Pending Replies</h3>
<div className="bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200 px-3 py-1 rounded-full text-sm font-medium">
{pendingReplies.length} pending
{filteredPendingReplies.length} pending
</div>
</div>

Expand All @@ -70,7 +74,7 @@ export function PendingRepliesList({ communityId }: PendingRepliesListProps) {
</Alert>

<div className="space-y-4">
{pendingReplies.map((reply) => (
{filteredPendingReplies.map((reply) => (
<PendingReplyItem
key={reply.id}
reply={reply}
Expand Down
22 changes: 17 additions & 5 deletions src/components/groups/PostList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ReplyList } from "./ReplyList";
import { ReportDialog } from "./ReportDialog";
import { shareContent } from "@/lib/share";
import { KINDS } from "@/lib/nostr-kinds";
import { filterSpamPosts } from "@/lib/spam-filter";
import {
DropdownMenu,
DropdownMenuContent,
Expand Down Expand Up @@ -141,12 +142,17 @@ export function PostList({ communityId, showOnlyApproved = false, pendingOnly =
approval: { id: string; pubkey: string; created_at: number; kind: number }
} => 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,
});
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/components/groups/ReplyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand All @@ -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
Expand Down
86 changes: 86 additions & 0 deletions src/lib/spam-filter.ts
Original file line number Diff line number Diff line change
@@ -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<T extends { tags: Array<Array<string>> }>(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<T extends { content: string }>(posts: T[]): T[] {
return posts.filter(post => !isSpamPost(post.content));
}
6 changes: 5 additions & 1 deletion src/pages/Groups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down