返回 DeepSeek-Reasonix
antispam.ts
根目录 / workers / forum / src / antispam.ts
1 // Anti-spam gate. Identity is verified upstream (id.reasonix.io); this layer
2 // decides whether a *verified* member may create content right now, using trust
3 // levels rather than per-post content scanning — so it stays cheap and the cost
4 // sits at the identity/first-post gate, not on every message.
5 import type { Member } from "./env";
6 import { HttpError } from "./identity";
7
8 // Trust rises with real participation; each level unlocks capabilities. New
9 // members (0) are deliberately limited so a throwaway account can't spam.
10 export const TRUST = { NEW: 0, BASIC: 1, MEMBER: 2, REGULAR: 3, LEADER: 4 } as const;
11
12 // Posts/day cap by trust — a soft brake on flooding from fresh accounts.
13 export function dailyPostCap(trust: number): number {
14 if (trust <= TRUST.NEW) return 5;
15 if (trust === TRUST.BASIC) return 20;
16 if (trust === TRUST.MEMBER) return 60;
17 return Infinity;
18 }
19
20 // Auto-hide a post once this many distinct members flag it, pending mod review.
21 export const AUTO_HIDE_FLAGS = 4;
22
23 const URL_RE = /\bhttps?:\/\/|\bwww\.|[a-z0-9-]+\.(com|net|io|org|cn|xyz|top|shop|vip)\b/i;
24
25 export function containsLink(body: string): boolean {
26 return URL_RE.test(body);
27 }
28
29 // Throws an HttpError when the member may not post. `minTrust` is the category
30 // gate; `body` enables the new-member link block.
31 export function assertCanPost(member: Member, opts: { minTrust: number; body: string }): void {
32 assertCanInteract(member);
33 if (isStaff(member)) return;
34 if (member.trust < opts.minTrust) {
35 throw new HttpError(403, "insufficient_trust", "You don't have access to post in this category yet.");
36 }
37 if (member.trust < TRUST.BASIC && containsLink(opts.body)) {
38 throw new HttpError(403, "links_restricted", "New members can't post links yet — this unlocks once you've participated a little.");
39 }
40 }
41
42 // Reactions and reports affect other people's content and moderation state, so
43 // they require the same verified, unsilenced identity as content creation.
44 export function assertCanInteract(member: Member): void {
45 if (!member.emailVerified) {
46 throw new HttpError(403, "email_unverified", "Confirm your email address before participating.");
47 }
48 if (member.silencedUntil && member.silencedUntil > new Date().toISOString()) {
49 throw new HttpError(403, "silenced", "Your account is temporarily restricted from participating.");
50 }
51 }
52
53 export function assertCanFlag(member: Member, postAuthor: string): void {
54 assertCanInteract(member);
55 if (member.email === postAuthor) {
56 throw new HttpError(422, "self_flag", "You can't report your own post.");
57 }
58 }
59
60 function isStaff(member: Member): boolean {
61 return member.role === "admin" || member.role === "moderator";
62 }
63
64 // Whether a member's action is subject to the per-IP creation limiter. Staff and
65 // trusted members (regular+) skip it; new/basic members are always rate-limited.
66 export function rateLimited(member: Member): boolean {
67 return !isStaff(member) && member.trust < TRUST.REGULAR;
68 }
69
69 lines TYPESCRIPT