cfff74e2db
Souq Marketplace: - Native souq pages: browse+filters, create listing, detail+seller profile, orders+chat - New API: reviews (POST), sellers/[id], upload (multipart), polar-checkout webhook - Listings API enhanced: minPrice, maxPrice, sortBy, deliveryDays filters - Seller workflow: start order, mark delivered, upload files, confirm delivery - Wallet: 5 Polar.sh FLH tiers, production checkout, mock fallback, success banner - Fix: order redirect /souq/history → /souq/orders Auth System: - Unified auth page, removed OAuth provider routing (2 files deleted) - Deleted oauth.ts (319 lines) — simplified auth chain - Streamlined login/register API routes Prisma Schema (+179 lines): - New models: Listing, Purchase, Review, Group/GroupMember - Gamification: DailyStreak, XpTransaction, Achievement, UserLevel - Learning: LearnCourse/Module/Enrollment/Certificate - Notifications, Referrals, Dhikr, PremiumBenefits Deleted legacy code: - src/app/api/marketplace/* (3 files) — replaced by /api/souq/* - src/app/api/files/[listingId]/route.ts — replaced by /api/souq/upload - src/app/api/seller/[sellerId]/route.ts — replaced by /api/souq/sellers/[id] Infrastructure: - Dockerfile: standalone output, Prisma runtime migration - docker-compose.yml: Polar env vars, healthcheck fix - docker-compose.staging.yml: staging on port 4014 - .gitea/workflows/ci.yml: CI pipeline
250 lines
9.1 KiB
TypeScript
250 lines
9.1 KiB
TypeScript
// Shariah-compliant auto-moderation for Falah forum content
|
|
// Uses keyword / simple pattern matching — not AI-based.
|
|
|
|
export interface ModerationResult {
|
|
status: "approved" | "flagged";
|
|
flags: string[];
|
|
flaggedWords: string[];
|
|
}
|
|
|
|
// ─── Rule definitions ───────────────────────────────────────────────────────
|
|
|
|
interface Rule {
|
|
name: string;
|
|
label: string;
|
|
/** Individual words/phrases to match with word-boundary regex */
|
|
words: string[];
|
|
}
|
|
|
|
const RULES: Rule[] = [
|
|
// ── Explicit / profane language ──────────────────────────────────────────
|
|
{
|
|
name: "profanity",
|
|
label: "Profanity / explicit language",
|
|
words: [
|
|
// English
|
|
"fuck", "shit", "asshole", "bitch", "bastard", "cunt",
|
|
"dickhead", "motherfucker", "pissed off", "slut", "whore",
|
|
"wanker", "douchebag", "cock sucker",
|
|
// Common Malay profanity (written in Latin script)
|
|
"babi", "pukimak", "bangsat", "celaka", "taik",
|
|
"mampus", "haram jadah", "anak haram", "keparat",
|
|
"paloi", "palia",
|
|
// Common Arabic profanity (Latin script)
|
|
"kalb", "himar", "qahba", "ahmaq", "yakhra",
|
|
],
|
|
},
|
|
|
|
// ── Hate speech / racial slurs ───────────────────────────────────────────
|
|
{
|
|
name: "hate_speech",
|
|
label: "Hate speech / racial slur",
|
|
words: [
|
|
"nigger", "kike", "spic", "chink", "gook", "wetback",
|
|
"coon", "paki", "towelhead", "raghead", "camel jockey",
|
|
],
|
|
},
|
|
|
|
// ── Riba / interest ──────────────────────────────────────────────────────
|
|
{
|
|
name: "riba",
|
|
label: "Promotion of riba / interest-based transactions",
|
|
words: [
|
|
"riba", "ribawi", "usury", "usurious",
|
|
],
|
|
},
|
|
|
|
// ── Maysir / gambling ────────────────────────────────────────────────────
|
|
{
|
|
name: "gambling",
|
|
label: "Promotion of gambling / maysir",
|
|
words: [
|
|
"maysir", "gambling", "gamble",
|
|
"casino", "slot machine", "poker",
|
|
"blackjack", "roulette", "lottery",
|
|
"bookmaker", "betting",
|
|
],
|
|
},
|
|
|
|
// ── Khamr / alcohol ──────────────────────────────────────────────────────
|
|
{
|
|
name: "alcohol",
|
|
label: "Promotion of alcohol / khamr",
|
|
words: [
|
|
"khamr", "alcohol", "beer", "wine", "vodka",
|
|
"whiskey", "whisky", "liquor", "intoxicant",
|
|
],
|
|
},
|
|
|
|
// ── Drugs ────────────────────────────────────────────────────────────────
|
|
{
|
|
name: "drugs",
|
|
label: "Promotion of drugs / intoxicants",
|
|
words: [
|
|
"marijuana", "cocaine", "heroin", "methamphetamine",
|
|
"shabu", "ecstasy", "opium", "cannabis", "hashish",
|
|
"crack cocaine", "ketamine", "lsd", "meth",
|
|
"amphetamine",
|
|
],
|
|
},
|
|
|
|
// ── Zina / adultery ──────────────────────────────────────────────────────
|
|
{
|
|
name: "zina",
|
|
label: "Promotion of zina / adultery / fornication",
|
|
words: [
|
|
"zina", "adultery", "fornication",
|
|
"prostitution", "prostitute", "escort service",
|
|
],
|
|
},
|
|
|
|
// ── Pornography ──────────────────────────────────────────────────────────
|
|
{
|
|
name: "pornography",
|
|
label: "Pornographic / obscene content",
|
|
words: [
|
|
"porn", "pornography", "xxx",
|
|
"onlyfans", "adult content",
|
|
],
|
|
},
|
|
|
|
// ── Shirk / kufr statements ──────────────────────────────────────────────
|
|
{
|
|
name: "shirk_kufr",
|
|
label: "Clear shirk / kufr statement",
|
|
words: [
|
|
"allah is not real", "allah does not exist",
|
|
"there is no god", "god is not allah",
|
|
"muhammad is false", "islam is false",
|
|
"quran is false", "koran is false",
|
|
"worship shaitan", "worship satan",
|
|
"allah is a lie",
|
|
],
|
|
},
|
|
];
|
|
|
|
// ── Helper: build a combined word-boundary regex for a rule ────────────────
|
|
|
|
function buildRuleRegex(words: string[]): RegExp {
|
|
// Escape special regex chars, then join as alternation
|
|
const escaped = words.map((w) =>
|
|
w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
);
|
|
return new RegExp(`\\b(${escaped.join("|")})\\b`, "gi");
|
|
}
|
|
|
|
// ── Spam detection helpers ─────────────────────────────────────────────────
|
|
|
|
function hasExcessiveUrls(text: string): boolean {
|
|
const urlMatches = text.match(/https?:\/\/[^\s]+/g);
|
|
return urlMatches !== null && urlMatches.length >= 3;
|
|
}
|
|
|
|
function hasRepetitiveText(text: string): boolean {
|
|
const words = text.toLowerCase().split(/\s+/);
|
|
const freq: Record<string, number> = {};
|
|
for (const word of words) {
|
|
// Skip very short words to reduce false positives
|
|
if (word.length < 3) continue;
|
|
freq[word] = (freq[word] || 0) + 1;
|
|
}
|
|
// Flag if the same word appears more than 8 times
|
|
return Object.values(freq).some((count) => count > 8);
|
|
}
|
|
|
|
function hasExcessiveCaps(text: string): boolean {
|
|
// Ignore very short texts
|
|
if (text.length < 30) return false;
|
|
const letters = text.replace(/[^a-zA-Z]/g, "");
|
|
if (letters.length < 10) return false;
|
|
const upper = letters.replace(/[a-z]/g, "").length;
|
|
return upper / letters.length > 0.7;
|
|
}
|
|
|
|
// ── Harassment detection ───────────────────────────────────────────────────
|
|
|
|
function hasHarassment(text: string): boolean {
|
|
const lower = text.toLowerCase();
|
|
const threatPatterns = [
|
|
/i will kill/i,
|
|
/i will hurt/i,
|
|
/i will destroy/i,
|
|
/i will beat/i,
|
|
/i (?:will|am going to) (?:murder|kill|harm)/i,
|
|
/(?:shut up|stfu)\s+(?:bitch|bastard|fuck)/i,
|
|
];
|
|
return threatPatterns.some((p) => p.test(lower));
|
|
}
|
|
|
|
// ── Main moderation function ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Moderate forum content against Shariah compliance rules.
|
|
*
|
|
* @param content - The body text of the thread or post.
|
|
* @param title - Optional title (for thread moderation).
|
|
* @returns An object with the moderation verdict, matching rule labels,
|
|
* and the specific words/phrases that triggered flags.
|
|
*/
|
|
export function moderateContent(
|
|
content: string,
|
|
title?: string,
|
|
): ModerationResult {
|
|
const text = title ? `${title}\n${content}` : content;
|
|
const flags: string[] = [];
|
|
const flaggedWords: string[] = [];
|
|
|
|
// ── Check keyword-based rules ──────────────────────────────────────────
|
|
for (const rule of RULES) {
|
|
const regex = buildRuleRegex(rule.words);
|
|
const matches = [...text.matchAll(regex)];
|
|
if (matches.length > 0) {
|
|
flags.push(rule.label);
|
|
for (const m of matches) {
|
|
if (!flaggedWords.includes(m[0].toLowerCase())) {
|
|
flaggedWords.push(m[0].toLowerCase());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Check harassment patterns ──────────────────────────────────────────
|
|
if (hasHarassment(text)) {
|
|
flags.push("Harassment / bullying");
|
|
if (!flaggedWords.includes("(harassment pattern)")) {
|
|
flaggedWords.push("(harassment pattern)");
|
|
}
|
|
}
|
|
|
|
// ── Check spam patterns ────────────────────────────────────────────────
|
|
let spamFlag = false;
|
|
if (hasExcessiveUrls(text)) {
|
|
spamFlag = true;
|
|
if (!flaggedWords.includes("(excessive URLs)")) {
|
|
flaggedWords.push("(excessive URLs)");
|
|
}
|
|
}
|
|
if (hasRepetitiveText(text)) {
|
|
spamFlag = true;
|
|
if (!flaggedWords.includes("(repetitive text)")) {
|
|
flaggedWords.push("(repetitive text)");
|
|
}
|
|
}
|
|
if (hasExcessiveCaps(text)) {
|
|
spamFlag = true;
|
|
if (!flaggedWords.includes("(excessive caps)")) {
|
|
flaggedWords.push("(excessive caps)");
|
|
}
|
|
}
|
|
if (spamFlag) {
|
|
flags.push("Spam patterns");
|
|
}
|
|
|
|
// ── No flags → approved ────────────────────────────────────────────────
|
|
if (flags.length === 0) {
|
|
return { status: "approved", flags: [], flaggedWords: [] };
|
|
}
|
|
|
|
return { status: "flagged", flags, flaggedWords };
|
|
}
|