Skip to content

Instantly share code, notes, and snippets.

@senrecep
Last active May 25, 2026 14:14
Show Gist options
  • Select an option

  • Save senrecep/c70e44b1cb88352aa14deb8c12c43f8b to your computer and use it in GitHub Desktop.

Select an option

Save senrecep/c70e44b1cb88352aa14deb8c12c43f8b to your computer and use it in GitHub Desktop.
Prototürk Forum — Spam Tespiti ve Trend Algoritması Rehberi (Reddit, HackerNews, Twitter algoritmalarından derlendi)

Prototürk Forum — Spam Tespiti ve Trend Algoritması Rehberi

Araştırma: Reddit, HackerNews, Twitter (X) açık kaynak algoritmalarından derlendi.
Hazırlayan: Prototürk (prototurk.com) Tayfun Erbilen için Hazırlayan: Recep Şen


1. Problem Tanımı

İki temel saldırı vektörü:

Saldırı Örnek Tehlike
Duplicate Post Spam Aynı içerik 50 kez paylaşılıyor Trend manipülasyonu
Comment Ring Bombing 20 sahte hesap aynı posta yorum/beğeni yağdırıyor Yapay trend

2. Büyük Platformların Formülleri

2.1 HackerNews — Gravity Formülü

Açık kaynak Arc kodu: Score = (P - 1)^0.8 / (T + 2)^G

Değişken Değer Açıklama
P int Post aldığı toplam upvote sayısı
P - 1 int Kendi oyunu iptal eder
T float Postun yaşı (saat cinsinden)
T + 2 float Sıfıra bölmeyi önler, yeni postları korur
G 1.8 Gravity sabiti — çürüme hızı

Gravity Kılavuzu:

  • G = 1.2 → Post uzun süre görünür kalır (slow forum)
  • G = 1.8 → HN'in seçimi — 24 saatte skor %10'a düşer
  • G = 2.5 → Hızlı akan feed (Twitter tarzı)
function hnScore(votes: number, ageHours: number, gravity = 1.8): number {
  const adjustedVotes = Math.max(votes - 1, 0);
  return Math.pow(adjustedVotes, 0.8) / Math.pow(ageHours + 2, gravity);
}

2.2 Reddit — Hot Formülü

Hot = sign(s) × log10(max(|s|, 1)) + (unix_time - 1134028003) / 45000
  • s = upvotes - downvotes
  • 45000 saniye = 12.5 saat → Her 12.5 saatlik periyot, 1 oy büyüklüğüne denk
  • Logaritmik sıkıştırma: 1→10 oy, 10→100 oy ile aynı ağırlığa sahip
function redditHot(ups: number, downs: number, date: Date): number {
  const REDDIT_EPOCH = 1134028003; // Dec 8, 2005
  const s = ups - downs;
  const order = Math.log10(Math.max(Math.abs(s), 1));
  const sign = s > 0 ? 1 : s < 0 ? -1 : 0;
  const seconds = date.getTime() / 1000 - REDDIT_EPOCH;
  return Math.round((sign * order + seconds / 45000) * 1e7) / 1e7;
}

Kritik özellik: Log compression doğal olarak spam'e karşı direnç sağlar. 10 sahte oy almak 1→10 arası fark yaratır. 10→100 aynı farkı yaratmak için 90 ek oy lazım.


2.3 Wilson Score — Yorum Kalite Sıralaması

score = (p̂ + z²/2n - z√(p̂(1-p̂)/n + z²/4n²)) / (1 + z²/n)
  • p̂ = upvotes / (upvotes + downvotes)
  • n = total_votes, z = 1.96 (95% güven aralığı)
function wilsonScore(ups: number, downs: number, z = 1.96): number {
  const n = ups + downs;
  if (n === 0) return 0;

  const pHat = ups / n;
  const z2 = z * z;
  const left = pHat + z2 / (2 * n);
  const right = z * Math.sqrt((pHat * (1 - pHat)) / n + z2 / (4 * n * n));
  const bottom = 1 + z2 / n;

  return (left - right) / bottom;
}

// Kullanım: yorumları en kaliteliden sırala
function sortComments<T extends { upvotes: number; downvotes: number }>(
  comments: T[]
): T[] {
  return [...comments].sort(
    (a, b) =>
      wilsonScore(b.upvotes, b.downvotes) - wilsonScore(a.upvotes, a.downvotes)
  );
}

2.4 Twitter Heavy Ranker — Multi-Task Weighted Score

Twitter'ın approximate production ağırlıkları (community reverse-engineering):

Engagement Ağırlık
Like 0.5
Retweet / Share 1.0
Reply 27.0
Yazar reply'e cevap verdi 75.0
Profil tıklandı + engage edildi 11.0
Negatif feedback negatif
Rapor edildi negatif
interface PostEngagement {
  likes: number;
  shares: number;
  comments: number;
  authorReplies: number;    // yazarın kendi postundaki yorumlara verdiği cevaplar
  bookmarks: number;
  reports: number;
  hides: number;
}

function twitterStyleWeightedScore(engagement: PostEngagement): number {
  return (
    engagement.likes * 0.5 +
    engagement.shares * 1.0 +
    engagement.comments * 5.0 +        // forum için comment daha değerli
    engagement.authorReplies * 15.0 +  // yazar engage olunca engagement yüksek
    engagement.bookmarks * 2.0 -
    engagement.reports * 20.0 -        // rapor büyük ceza
    engagement.hides * 5.0
  );
}

3. Spam Tespit Sistemi

3.1 Katmanlı Savunma Mimarisi

POST GELİYOR
    │
    ▼
[KATMAN 1: RATE LIMITER] ── aynı IP/kullanıcıdan çok hızlı mı?
    │ geçtiyse
    ▼
[KATMAN 2: DUPLICATE DETECTOR] ── bu içerik daha önce paylaşıldı mı?
    │ geçtiyse
    ▼
[KATMAN 3: BEHAVIORAL CHECK] ── hesap yeni/şüpheli mi?
    │ geçtiyse
    ▼
[KATMAN 4: SPAM CLASSIFIER] ── içerik spam pattern'ı var mı?
    │ geçtiyse
    ▼
POST KABUL EDİLDİ (spam_score ile birlikte)

3.2 Katman 1: Rate Limiter (Redis ile Sliding Window)

import { createClient } from "redis";

const redis = createClient();

interface RateLimitResult {
  allowed: boolean;
  reason?: string;
}

async function canPost(
  userId: string,
  limit = 3,
  windowSeconds = 300
): Promise<RateLimitResult> {
  const now = Date.now() / 1000;
  const key = `post_rate:${userId}`;

  await redis.zRemRangeByScore(key, 0, now - windowSeconds);

  const count = await redis.zCard(key);
  if (count >= limit) {
    return {
      allowed: false,
      reason: `Rate limit: ${windowSeconds / 60} dakikada max ${limit} post`,
    };
  }

  await redis.zAdd(key, { score: now, value: String(now) });
  await redis.expire(key, windowSeconds);
  return { allowed: true };
}

async function canComment(
  userId: string,
  postId: string,
  limit = 10,
  windowSeconds = 60
): Promise<boolean> {
  const now = Date.now() / 1000;
  const key = `comment_rate:${userId}:${postId}`;

  await redis.zRemRangeByScore(key, 0, now - windowSeconds);
  const count = await redis.zCard(key);

  if (count >= limit) return false;

  await redis.zAdd(key, { score: now, value: String(now) });
  await redis.expire(key, windowSeconds);
  return true;
}

3.3 Katman 2: Duplicate Post Tespiti

Tam kopya tespiti (SHA-256 hash):

import crypto from "crypto";

async function isExactDuplicate(
  userId: string,
  content: string,
  windowSeconds = 86400 // 24 saat
): Promise<boolean> {
  const normalized = content.trim().toLowerCase();
  const hash = crypto
    .createHash("sha256")
    .update(normalized)
    .digest("hex");
  const key = `content_hash:${userId}:${hash}`;

  const exists = await redis.exists(key);
  if (exists) return true;

  await redis.setEx(key, windowSeconds, "1");
  return false;
}

Yakın kopya tespiti (Jaccard Similarity):

function getShingles(text: string, n = 3): Set<string> {
  const normalized = text.toLowerCase().trim();
  const shingles = new Set<string>();
  for (let i = 0; i <= normalized.length - n; i++) {
    shingles.add(normalized.slice(i, i + n));
  }
  return shingles;
}

function jaccardSimilarity(textA: string, textB: string, n = 3): number {
  const a = getShingles(textA, n);
  const b = getShingles(textB, n);

  const intersection = new Set([...a].filter((x) => b.has(x)));
  const union = new Set([...a, ...b]);

  return union.size === 0 ? 0 : intersection.size / union.size;
}

function isNearDuplicate(
  newContent: string,
  recentPosts: Array<{ id: string; content: string }>,
  threshold = 0.75
): { isDuplicate: boolean; matchedPostId?: string } {
  for (const post of recentPosts) {
    if (jaccardSimilarity(newContent, post.content) >= threshold) {
      return { isDuplicate: true, matchedPostId: post.id };
    }
  }
  return { isDuplicate: false };
}

3.4 Katman 3: Hesap Güven Skoru

interface User {
  createdAt: Date;
  spamFlagCount: number;
  approvedPosts: number;
  isEmailVerified: boolean;
}

function accountTrustScore(user: User): number {
  const now = new Date();
  const ageDays =
    (now.getTime() - user.createdAt.getTime()) / (1000 * 60 * 60 * 24);

  // Yaş faktörü: 30 günde ~0.63, 90 günde ~0.95
  const ageFactor = 1 - Math.exp(-ageDays / 30);

  // Spam cezası: Her flag %40 azaltır
  const spamFactor = Math.exp(-user.spamFlagCount * 0.5);

  // İyi içerik bonusu: Logaritmik
  const goodPostsFactor = Math.min(Math.log1p(user.approvedPosts) / 10, 1.0);

  // Email doğrulama bonusu
  const emailBonus = user.isEmailVerified ? 1.0 : 0.7;

  const trust =
    ageFactor * spamFactor * (0.5 + 0.5 * goodPostsFactor) * emailBonus;

  return Math.max(0.01, Math.min(1.0, trust));
}

interface Vote {
  userId: string;
  value: 1 | -1;
  user: User;
}

function weightedVoteScore(votes: Vote[]): number {
  return votes.reduce((total, vote) => {
    const trust = accountTrustScore(vote.user);
    return total + vote.value * trust;
  }, 0);
}

3.5 Katman 4: Koordineli Spam Tespiti (Ring Voting)

interface Action {
  userId: string;
  postId: string;
  timestamp: number; // unix ms
}

interface CoActivityEdge {
  users: [string, string];
  weight: number;
}

function detectVoteRing(
  actions: Action[],
  timeWindowMs = 60_000,  // 1 dakika
  minEdgeWeight = 5       // 5 ortak aksiyon = ring
): { suspiciousUsers: Set<string>; edges: CoActivityEdge[] } {
  // Post bazında aksiyonları grupla
  const byPost = new Map<string, Array<{ userId: string; timestamp: number }>>();
  for (const action of actions) {
    if (!byPost.has(action.postId)) byPost.set(action.postId, []);
    byPost.get(action.postId)!.push({
      userId: action.userId,
      timestamp: action.timestamp,
    });
  }

  // Co-activity graph oluştur
  const coActivity = new Map<string, number>();

  for (const [, userTimes] of byPost) {
    userTimes.sort((a, b) => a.timestamp - b.timestamp);

    for (let i = 0; i < userTimes.length; i++) {
      for (let j = i + 1; j < userTimes.length; j++) {
        const { userId: u1, timestamp: t1 } = userTimes[i];
        const { userId: u2, timestamp: t2 } = userTimes[j];

        if (Math.abs(t2 - t1) <= timeWindowMs) {
          const edgeKey = [u1, u2].sort().join("|");
          coActivity.set(edgeKey, (coActivity.get(edgeKey) ?? 0) + 1);
        }
      }
    }
  }

  // Eşik üzerindeki kenarları bul
  const suspiciousEdges: CoActivityEdge[] = [];
  const suspiciousUsers = new Set<string>();

  for (const [edgeKey, weight] of coActivity) {
    if (weight >= minEdgeWeight) {
      const [u1, u2] = edgeKey.split("|") as [string, string];
      suspiciousEdges.push({ users: [u1, u2], weight });
      suspiciousUsers.add(u1);
      suspiciousUsers.add(u2);
    }
  }

  return { suspiciousUsers, edges: suspiciousEdges };
}

4. Prototürk Ana Trend Skoru

4.1 Spam Ceza Çarpanı

interface PostSpamSignals {
  voteIpConcentration: number;  // 0-1: oyların ne kadarı aynı subnet'ten
  voteVelocityZscore: number;   // istatistiksel anormallik skoru
  newAccountRatio: number;      // oy verenlerin ne kadarı <7 gün hesap
  isDuplicate: boolean;
  reportCount: number;
}

function calculateSpamPenalty(signals: PostSpamSignals): number {
  let penalty = 1.0;

  // IP konsantrasyonu: Oyların %70'i aynı /24 subnet'ten
  if (signals.voteIpConcentration > 0.7) {
    penalty *= 0.2;
  }

  // Hız anomalisi: Z-score > 3 = istatistiksel outlier
  if (signals.voteVelocityZscore > 3.0) {
    const reduction = Math.max(0.1, 1 - (signals.voteVelocityZscore - 3) * 0.1);
    penalty *= reduction;
  }

  // Yeni hesap oranı: %60'tan fazla yeni hesap
  if (signals.newAccountRatio > 0.6) {
    penalty *= 0.3;
  }

  // Duplicate içerik
  if (signals.isDuplicate) {
    penalty *= 0.1;
  }

  // Rapor sayısı
  if (signals.reportCount > 5) {
    penalty *= Math.max(0.05, 1 - signals.reportCount * 0.1);
  }

  return Math.max(0.0, Math.min(1.0, penalty));
}

4.2 Ana Trend Skoru

interface PostMetrics {
  weightedVoteScore: number;   // weightedVoteScore() sonucu
  commentCount: number;
  uniqueViewers: number;
  bookmarkCount: number;
  reportCount: number;
  hideCount: number;
  createdAt: Date;
  spamSignals: PostSpamSignals;
}

function prototurkTrendScore(
  metrics: PostMetrics,
  gravity = 1.8
): number {
  const now = new Date();
  const ageHours =
    (now.getTime() - metrics.createdAt.getTime()) / (1000 * 60 * 60);

  // 1. Ham engagement skoru
  const rawScore =
    metrics.weightedVoteScore * 1.0 +
    metrics.commentCount * 3.0 +
    metrics.uniqueViewers * 0.1 +
    metrics.bookmarkCount * 2.0 -
    metrics.reportCount * 15.0 -
    metrics.hideCount * 3.0;

  // 2. Spam cezası uygula
  const spamPenalty = calculateSpamPenalty(metrics.spamSignals);
  const adjustedScore = Math.max(rawScore * spamPenalty, 0);

  // 3. HN Gravity ile zaman çürümesi
  return Math.pow(adjustedScore, 0.8) / Math.pow(ageHours + 2, gravity);
}

4.3 Oy Hız Analizi (Velocity Z-Score)

function calculateVelocityZscore(
  voteCounts: number[] // son N saatteki oylama sayıları (saatlik dilimlerde)
): number {
  if (voteCounts.length < 2) return 0;

  const mean = voteCounts.reduce((a, b) => a + b, 0) / voteCounts.length;
  const variance =
    voteCounts.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) /
    voteCounts.length;
  const stddev = Math.sqrt(variance);

  if (stddev === 0) return 0;

  const current = voteCounts[voteCounts.length - 1];
  return (current - mean) / stddev;
  // z > 3.0 = şüpheli burst
}

5. Shadowban Stratejisi

Reddit'in yaklaşımı — ban yerine sessizce etkisiz kıl:

interface ShadowbanOptions {
  userId: string;
  reason: string;
  db: DatabaseClient; // kendi ORM/client'ın
}

async function applyShadowban({
  userId,
  reason,
  db,
}: ShadowbanOptions): Promise<void> {
  /*
   * Kullanıcı normal görünür ama:
   * - Trend skoruna katkı yapmaz
   * - Diğer kullanıcılara gösterilmez
   * - Kullanıcı kendisi görür (direni azaltır)
   */
  await db.users.update({
    where: { id: userId },
    data: {
      isShadowbanned: true,
      shadowbanReason: reason,
      shadowbannedAt: new Date(),
    },
  });

  // Geçmiş vote ağırlıklarını sıfırla
  await db.votes.updateMany({
    where: { userId },
    data: { weight: 0.0 },
  });
}

// İçerik sorgularında shadowban filtresi
function buildPostQuery(viewerUserId: string) {
  return {
    where: {
      OR: [
        { author: { isShadowbanned: false } },
        { authorId: viewerUserId }, // kendi postlarını görebilir
      ],
    },
  };
}

6. Veritabanı Schema (Prisma)

model Post {
  id                    String   @id @default(cuid())
  content               String
  authorId              String
  author                User     @relation(fields: [authorId], references: [id])
  createdAt             DateTime @default(now())

  // Trend metrikleri
  trendScore            Float    @default(0)
  spamPenalty           Float    @default(1.0)   // 1.0 = temiz
  weightedVoteScore     Float    @default(0)

  // Spam sinyalleri
  voteIpConcentration   Float    @default(0)
  voteVelocityZscore    Float    @default(0)
  newAccountVoteRatio   Float    @default(0)
  isDuplicate           Boolean  @default(false)

  votes                 Vote[]
  comments              Comment[]

  @@index([trendScore(sort: Desc)])
}

model User {
  id               String   @id @default(cuid())
  createdAt        DateTime @default(now())

  // Güven sistemi
  trustScore       Float    @default(0.3)
  spamFlagCount    Int      @default(0)
  approvedPosts    Int      @default(0)
  isShadowbanned   Boolean  @default(false)
  shadowbanReason  String?
  isEmailVerified  Boolean  @default(false)

  posts            Post[]
  votes            Vote[]
}

model Vote {
  id                  String   @id @default(cuid())
  postId              String
  post                Post     @relation(fields: [postId], references: [id])
  userId              String
  user                User     @relation(fields: [userId], references: [id])
  value               Int      // +1 veya -1
  weight              Float    @default(1.0)    // accountTrustScore sonucu
  voterTrustAtTime    Float    @default(1.0)
  voterIp             String?
  createdAt           DateTime @default(now())

  @@unique([postId, userId])
}

7. Cron Job — Trend Skorlarını Yenile

// Her 5 dakikada bir çalıştır (cron: "*/5 * * * *")
async function refreshTrendingScores(db: DatabaseClient): Promise<void> {
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);

  const activePosts = await db.posts.findMany({
    where: { createdAt: { gte: sevenDaysAgo } },
    include: {
      votes: { include: { user: true } },
      _count: { select: { comments: true } },
    },
  });

  const updates = activePosts.map((post) => {
    const ageHours =
      (Date.now() - post.createdAt.getTime()) / (1000 * 60 * 60);

    const voteScore = weightedVoteScore(
      post.votes.map((v) => ({ ...v, value: v.value as 1 | -1 }))
    );

    const spamSignals: PostSpamSignals = {
      voteIpConcentration: post.voteIpConcentration,
      voteVelocityZscore: post.voteVelocityZscore,
      newAccountRatio: post.newAccountVoteRatio,
      isDuplicate: post.isDuplicate,
      reportCount: 0, // ayrıca say
    };

    const trendScore = prototurkTrendScore(
      {
        weightedVoteScore: voteScore,
        commentCount: post._count.comments,
        uniqueViewers: 0,
        bookmarkCount: 0,
        reportCount: 0,
        hideCount: 0,
        createdAt: post.createdAt,
        spamSignals,
      },
      1.8 // gravity
    );

    return db.posts.update({
      where: { id: post.id },
      data: { trendScore, weightedVoteScore: voteScore },
    });
  });

  await Promise.all(updates);
  console.log(`[TREND] ${activePosts.length} post güncellendi`);
}

8. Uygulama Öncelikleri

1. [ACİL]        Rate limiter        → Redis sliding window
2. [ACİL]        Exact duplicate     → SHA-256 hash kontrolü
3. [ORTA]        Account trust score → Hesap yaşı + flag sayısına göre
4. [ORTA]        HN Gravity formula  → Mevcut sıralamanın yerini alır
5. [UZUN VADELİ] Ring voting tespiti → Co-activity graph analizi
6. [UZUN VADELİ] Naive Bayes         → Forum büyüdükçe eğitilir

9. Ücretsiz/Hazır Araç Önerileri

Araç Amaç URL
Akismet API Yorum spam tespiti (Wordpress'in kullandığı) akismet.com/development
Redis Rate limiter + sliding window redis.io
Prisma ORM + schema yönetimi prisma.io
BullMQ Trend skoru güncelleme için queue bullmq.io

Kaynaklar

@senrecep

Copy link
Copy Markdown
Author

Prototürk Spam Tespiti — Kalibrasyon Analizi

Tayfun Erbilen'in mevcut spam sistemi incelendi. Sistem mimarisi doğru tasarlanmış (content hash + suspicion score + behavioral signals). Sorun algoritma değil, eşik değerlerinin küçük/orta ölçekli forum için kalibrasyonu.


Tespit Edilen 3 Darboğaz

Sorun 1 — Like Burst Eşiği Çok Yüksek

spam.behavioral.like_burst_count = 30  (60 saniyede 30 like)

Botlar daha yavaş çalışır — 60 saniyede 30 like şartı gerçekçi değil.
Öneri: 10–15 yap.


Sorun 2 — Cluster Eşiği Büyük Forum İçin Ayarlı

spam.content_hash.cluster_size = 5  (5 farklı kullanıcı aynı içerik)

Küçük toplulukta 5 kullanıcının aynı içeriği paylaşması zaten nadir → spam ring'ler kaçıyor.
Öneri: 2–3 yap.


Sorun 3 — Suspicion Score Çok Hızlı Eriyor

spam.score.decay_hours = 24   (24 saatte puan yarıya iner)
spam.score.suspend_threshold = 15

Bot günde 3–4 şüpheli eylem yapıp 24 saatte skor düşürse asla 15'e ulaşamaz.
Öneri: decay_hours = 72, suspend_threshold = 10


Önerilen Optimal Değerler (Küçük/Orta Forum)

Ayar Mevcut Öneri Neden
like_burst_count 30 10 Bot tespiti için daha hassas
like_target_focus_count 5 3 Tek yazara odaklı atağı yakala
cluster_size 5 3 Küçük ring'leri yakala
cluster_score 10 15 Ring tespiti ağır cezalandırılsın
score.decay_hours 24 72 Daha uzun hafıza
score.suspend_threshold 15 10 Daha çabuk askıya al
new_account_multiplier 0.5 0.3 Yeni hesaplar daha şüpheli
short_post_chars 50 100 Kısa spamları da yakala

Not: Hangi tür spam geçiyor? (aynı post tekrarı mı, yorum bombardımanı mı?) Buna göre ilgili katman daha agresif hale getirilebilir.

@senrecep

Copy link
Copy Markdown
Author

Güncelleme: Çift Saldırı Vektörü Tespiti

Durum: Hem aynı post tekrarı hem yorum bombardımanı mevcut → büyük ihtimalle koordineli bir ring (organize grup, tek kullanıcı değil).


Saldırı 1: Aynı Post Tekrarı → Content Hash Katmanı

Mevcut ayarlar bu saldırıyı kaçırıyor çünkü:

  • cluster_size = 5 → 5 farklı kullanıcı gerekiyor, küçük ring'ler (2-4 kişi) geçiyor
  • short_post_chars = 50 → 50 karakter altı postlar muaf, spam genellikle kısa

Acil değişiklikler:

spam.content_hash.cluster_size          5  →  2
spam.content_hash.cluster_score        10  →  20   (ring tespiti daha ağır cezalansın)
spam.content_hash.cross_author_score    6  →  12
spam.content_hash.short_post_chars     50  →  20   (çok kısa spamları da yakala)
spam.content_hash.cross_author_window_days  7  →  14

Saldırı 2: Yorum Bombardımanı → Behavioral + Rate Limiter Katmanı

Mevcut ayarlar yavaş botları kaçırıyor:

  • like_burst_count = 30 → 60 saniyede 30 like şartı çok yüksek
  • like_target_focus_count = 5 → tek yazara 5 like yetersiz
  • window_seconds = 60 → pencere çok dar, botlar aralıklı çalışıyor

Acil değişiklikler:

spam.behavioral.like_burst_count         30  →  8
spam.behavioral.like_burst_score         10  →  15
spam.behavioral.like_target_focus_count   5  →  3
spam.behavioral.like_target_focus_score   4  →  10
spam.behavioral.window_seconds           60  →  300  (5 dakikalık pencere)
spam.behavioral.new_account_multiplier  0.5  →  0.2  (yeni hesaplar çok daha şüpheli)

Suspicion Score — Ring Tespiti İçin Kritik

İki saldırı birden aktifse puan birikimi hızlanmalı, hafıza uzamalı:

spam.score.decay_hours        24  →  72   (3 günlük hafıza)
spam.score.suspend_threshold  15  →  8    (daha çabuk askıya al)
spam.score.flag_threshold      5  →  3    (admin'e daha erken bildir)
spam.score.window_days         7  →  30   (aylık geçmişe bak)

Koordineli Ring için Ek Önlem

Mevcut sistemde cross-user koordinasyon tespiti eksik görünüyor.
Şu sorguyu periyodik çalıştırmak ring'leri ortaya çıkarır:

-- Son 1 saatte aynı içeriği paylaşan kullanıcı grupları
SELECT content_hash, COUNT(DISTINCT user_id) as user_count, 
       array_agg(DISTINCT user_id) as users
FROM posts
WHERE created_at > NOW() - INTERVAL '1 hour'
GROUP BY content_hash
HAVING COUNT(DISTINCT user_id) >= 2
ORDER BY user_count DESC;

-- Aynı postlara koordineli yorum atan hesaplar
SELECT p1.user_id, p2.user_id, COUNT(*) as co_comment_count
FROM comments p1
JOIN comments p2 ON p1.post_id = p2.post_id 
  AND p1.user_id < p2.user_id
  AND ABS(EXTRACT(EPOCH FROM (p1.created_at - p2.created_at))) < 60
WHERE p1.created_at > NOW() - INTERVAL '24 hours'
GROUP BY p1.user_id, p2.user_id
HAVING COUNT(*) >= 3
ORDER BY co_comment_count DESC;

Bu sorgular ring üyelerini direkt listeler — manuel inceleme için başlangıç noktası.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment