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