-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
101 lines (89 loc) · 3.71 KB
/
Copy pathserver.js
File metadata and controls
101 lines (89 loc) · 3.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* Content Moderator, an APIVerve template.
*
* Masks profanity in comments, reviews and chat before they are shown.
* The API key stays on the server: the browser only ever talks to /api routes.
*
* Profanity Filter: https://apiverve.com/marketplace/profanityfilter
*/
const express = require('express');
const path = require('path');
// Set APIVERVE_API_KEY in .env (local) or your host's environment variables.
// Get a free key at https://dashboard.apiverve.com
const API_KEY = process.env.APIVERVE_API_KEY;
const PORT = process.env.PORT || 3000;
// ============================================
// Rate limit
// Once deployed, anyone who finds this URL can call it with YOUR key.
// This caps each visitor at RATE_LIMIT requests per minute. It is kept in
// memory, so it resets on cold starts and isn't shared between instances:
// good enough for a demo. For production, use a shared store (e.g. Upstash
// Redis) or put the app behind your own auth.
// ============================================
const RATE_LIMIT = 10;
const WINDOW_MS = 60_000;
const hits = new Map();
function rateLimited(ip) {
const now = Date.now();
const recent = (hits.get(ip) || []).filter((t) => now - t < WINDOW_MS);
recent.push(now);
hits.set(ip, recent);
if (hits.size > 5000) hits.clear();
return recent.length > RATE_LIMIT;
}
/** Calls an APIVerve API and returns its data, or throws with its error message. */
async function callApi(api, { query, body } = {}) {
const url = `https://api.apiverve.com/v1/${api}${query ? `?${new URLSearchParams(query)}` : ''}`;
const res = await fetch(url, {
method: body ? 'POST' : 'GET',
headers: { 'x-api-key': API_KEY, ...(body && { 'Content-Type': 'application/json' }) },
body: body && JSON.stringify(body)
});
const json = await res.json().catch(() => null);
if (!res.ok || json?.status !== 'ok') {
const err = json?.error;
const message = err?.missing ? `Missing: ${err.missing.join(', ')}` : typeof err === 'string' ? err : `APIVerve returned ${res.status}`;
throw Object.assign(new Error(message), { status: res.status === 429 ? 429 : 502 });
}
return json.data;
}
/** A trimmed string, capped at max characters. */
const str = (v, max) => String(v ?? '').trim().slice(0, max);
const app = express();
app.use(express.json({ limit: '20kb' }));
// Serves the page locally. On Vercel, public/ is served from the CDN instead.
app.use(express.static(path.join(__dirname, 'public')));
// Every /api route needs the key, and counts against the visitor's limit.
app.use('/api', (req, res, next) => {
if (!API_KEY) {
return res.status(500).json({ error: 'Missing APIVERVE_API_KEY. Add it to .env, or to your host’s environment variables, then restart.' });
}
const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket.remoteAddress || 'local';
if (rateLimited(ip)) {
return res.status(429).json({ error: 'Too many requests. Wait a minute and try again.' });
}
next();
});
const MASKS = ['*', '#', '-', '•'];
// POST /api/moderate { text, mask }
app.post('/api/moderate', async (req, res) => {
const text = str(req.body.text, 5000);
const mask = MASKS.includes(req.body.mask) ? req.body.mask : '*';
if (!text) return res.status(400).json({ error: 'Enter some text to check.' });
try {
const data = await callApi('profanityfilter', { body: { text, mask } });
res.json({
success: true,
original: text,
filtered: data.filteredText,
isProfane: data.isProfane,
profaneWords: data.profaneWords || 0,
mask: data.mask
});
} catch (err) {
res.status(err.status || 502).json({ error: err.message });
}
});
app.listen(PORT, () => {
console.log(`Content Moderator running at http://localhost:${PORT}`);
});