-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshared-api.mjs
More file actions
64 lines (59 loc) · 21.4 KB
/
Copy pathshared-api.mjs
File metadata and controls
64 lines (59 loc) · 21.4 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
const DID_PATTERN = /^did:key:[A-Za-z0-9_-]{20,200}$/;
const ROOM_PATTERN = /^[a-z0-9][a-z0-9_-]{0,47}$/;
const CONTRIBUTION_PATTERN = /\b(contribution|contribute|tool|guide|tutorial|how[- ]?to|workflow|template|repository|repo|open source)\b/i;
const OPPORTUNITY_RULES = [["Airdrop", /\bairdrop\b/i], ["Testnet", /\btestnet\b/i], ["Faucet", /\bfaucet\b/i], ["Claim", /\bclaim(?:ing|ed)?\b/i], ["Quest", /\b(?:new )?quest\b/i], ["Task", /\b(?:new\s+task|task\s+(?:available|live|open)|complete\s+(?:the\s+)?task|task\s*[::])/i], ["Reward", /\b(?:new )?rewards?\s+(?:available|live|claim|for)\b/i], ["Eligibility", /\beligi(?:ble|bility)\b/i], ["Participation", /\bparticipation\s+(?:requirements?|open|is open)\b/i]];
const DEADLINE_PATTERN = /\b(?:deadline|ends?|closing|due)\b[^\n.!?]{0,120}/i;
const REQUIREMENTS_PATTERN = /[^\n.!?]{0,90}\b(?:requirements?|eligible|eligibility|must|need to|participat(?:e|ion)|steps?)\b[^\n.!?]{0,160}/i;
const HEALTH_REPORT_PATTERN = /\b(?:still running|node healthy|node is healthy|all systems? (?:go|operational)|heartbeat|uptime|validator healthy|sync(?:ed|ing)? normally)\b/i;
const SUSPICIOUS_PATTERN = /\b(?:seed phrase|private key|secret key|wallet connect|connect (?:your )?wallet|sign (?:this|a) (?:message|transaction)|mnemonic)\b/i;
const URL_PATTERN = /https?:\/\/[^\s<>"']+/gi;
const REACTIONS = new Set(["useful", "noise", "needs_verification", "interesting"]);
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
const string = (value, max = 4096) => typeof value === "string" ? value.slice(0, max) : "";
const number = (value) => Number.isFinite(value) ? value : 0;
const timestamp = (value) => Number.isFinite(Date.parse(value)) ? Date.parse(value) : 0;
const compareNewest = (a, b) => timestamp(b.ts) - timestamp(a.ts) || b.seq - a.seq;
const json = (data, status = 200) => new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" } });
const error = (message, status = 404) => json({ error: message }, status);
const digestHash = (value) => {
let hash = 0xcbf29ce484222325n;
for (const char of value) { hash ^= BigInt(char.codePointAt(0)); hash = BigInt.asUintN(64, hash * 0x100000001b3n); }
return hash.toString(16).padStart(16, "0");
};
const sha256Hex = (value) => {
const source = new TextEncoder().encode(value), bitLength = source.length * 8;
const paddedLength = Math.ceil((source.length + 9) / 64) * 64, bytes = new Uint8Array(paddedLength);
bytes.set(source); bytes[source.length] = 0x80;
const view = new DataView(bytes.buffer); view.setUint32(paddedLength - 4, bitLength >>> 0); view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x1_0000_0000));
const k = [0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2];
const h = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], w = new Uint32Array(64), rotr = (x, n) => (x >>> n) | (x << (32 - n));
for (let offset = 0; offset < bytes.length; offset += 64) { for (let i = 0; i < 16; i += 1) w[i] = view.getUint32(offset + i * 4); for (let i = 16; i < 64; i += 1) { const a = w[i - 15], b = w[i - 2]; w[i] = (w[i - 16] + (rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3)) + w[i - 7] + (rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10))) >>> 0; } let [a,b,c,d,e,f,g,q] = h; for (let i = 0; i < 64; i += 1) { const s1 = rotr(e,6)^rotr(e,11)^rotr(e,25), choice = (e & f) ^ (~e & g), temp1 = (q + s1 + choice + k[i] + w[i]) >>> 0, s0 = rotr(a,2)^rotr(a,13)^rotr(a,22), majority = (a & b) ^ (a & c) ^ (b & c), temp2 = (s0 + majority) >>> 0; q=g; g=f; f=e; e=(d+temp1)>>>0; d=c; c=b; b=a; a=(temp1+temp2)>>>0; } h[0]=(h[0]+a)>>>0; h[1]=(h[1]+b)>>>0; h[2]=(h[2]+c)>>>0; h[3]=(h[3]+d)>>>0; h[4]=(h[4]+e)>>>0; h[5]=(h[5]+f)>>>0; h[6]=(h[6]+g)>>>0; h[7]=(h[7]+q)>>>0; }
return h.map((part) => part.toString(16).padStart(8, "0")).join("");
};
const normalizeCandidateText = (text) => text.toLowerCase().replace(URL_PATTERN, "[url]").replace(/\d+/g, "#").replace(/[^\p{L}\p{N}\[\]]+/gu, " ").trim().slice(0, 500);
const extractUrls = (text) => [...text.matchAll(URL_PATTERN)].map((match) => match[0].replace(/[),.;!?]+$/, "")).slice(0, 5);
const extractSignal = (text, pattern) => text.match(pattern)?.[0]?.trim() ?? null;
const publicRoom = (room) => { const { messages, ...summary } = room; return summary; };
const publicDid = (did) => ({ ...did, posts: did.posts.slice(0, 20), contributions: did.contributions.slice(0, 20) });
export function createApi({ origin = "https://technocore.chat", cacheTtlMs = 180_000, roomLimit = 200, operatorDid = null, feedbackStore, log = console }) {
let cache = { expiresAt: 0, value: null, refreshPromise: null };
let feedback = null;
async function loadFeedback() { if (feedback !== null) return feedback; feedback = await feedbackStore.load(); return feedback; }
async function feedbackFor(id) { const all = await loadFeedback(); return { useful: 0, noise: 0, needs_verification: 0, interesting: 0, ...(all[id] ?? {}) }; }
async function withFeedback(items) { return Promise.all(items.map(async (item) => ({ ...item, feedback: await feedbackFor(item.id) }))).then((entries) => entries.sort((a, b) => ((b.feedback.useful + b.feedback.interesting * .5 - b.feedback.noise * 1.5) - (a.feedback.useful + a.feedback.interesting * .5 - a.feedback.noise * 1.5)) || timestamp(b.discoveredAt) - timestamp(a.discoveredAt))); }
function normalizeMessage(raw, room) { if (!isRecord(raw)) return null; const ts = string(raw.ts, 80), text = string(raw.text); if (!ts || !text) return null; return { room, seq: number(raw.seq), ts, from: string(raw.from, 300), text, nonce: string(raw.nonce, 200) }; }
function normalizeRoom(raw) { if (!isRecord(raw) || !ROOM_PATTERN.test(string(raw.room, 48))) return null; return { room: raw.room, lastSeq: number(raw.last_seq), bytes: number(raw.bytes), idleSeconds: number(raw.idle_seconds), topic: string(raw.topic), window: number(raw.window), zeroResponseShare: typeof raw.zero_response_share === "number" ? raw.zero_response_share : null, nickDiversity: typeof raw.nick_diversity === "number" ? raw.nick_diversity : null }; }
function parseRoomEvents(raw) { const events = []; for (const line of raw.split("\n")) { const match = /^\[\d+\]\s+(\S+)\s+<~server>\s+created\s+([a-z0-9][a-z0-9_-]{0,47})$/.exec(line); if (match) events.push({ room: match[2], createdAt: match[1] }); } return events.sort((a, b) => timestamp(b.createdAt) - timestamp(a.createdAt)); }
async function mapConcurrent(items, mapper) { const results = new Array(items.length); let cursor = 0; await Promise.all(Array.from({ length: Math.min(6, items.length) }, async () => { while (true) { const index = cursor++; if (index >= items.length) return; results[index] = await mapper(items[index]); } })); return results; }
function calculateMetrics(room, messages, collectedAt) { const hour = 3_600_000; const recent = messages.filter((message) => collectedAt - timestamp(message.ts) <= hour); const sixHours = messages.filter((message) => collectedAt - timestamp(message.ts) <= 6 * hour); const day = messages.filter((message) => collectedAt - timestamp(message.ts) <= 24 * hour); const priorFiveHours = messages.filter((message) => { const age = collectedAt - timestamp(message.ts); return age > hour && age <= 6 * hour; }); const lastActivityAt = messages.reduce((latest, message) => Math.max(latest, timestamp(message.ts)), 0); const velocity = recent.length * 8 + sixHours.length * 2 + day.length * .25 + (lastActivityAt ? Math.max(0, 1 - (collectedAt - lastActivityAt) / hour) : 0); return { ...room, messages, sampledPostCount: messages.length, posts1h: recent.length, posts6h: sixHours.length, posts24h: day.length, velocity: Math.round(velocity * 100) / 100, growth: Math.round((recent.length - priorFiveHours.length / 5) * 100) / 100, lastActivityAt: lastActivityAt ? new Date(lastActivityAt).toISOString() : null }; }
function classifyOpportunity(message) { const types = OPPORTUNITY_RULES.filter(([, pattern]) => pattern.test(message.text)).map(([type]) => type); if (!types.length || (HEALTH_REPORT_PATTERN.test(message.text) && !/\b(?:airdrop|testnet|faucet|claim|quest|task|reward|eligib|participation)\b/i.test(message.text))) return null; const suspicious = SUSPICIOUS_PATTERN.test(message.text); const summary = message.text.replace(/\s+/g, " ").trim().slice(0, 280); return { id: digestHash(`${message.room}|${normalizeCandidateText(message.text)}`), type: types[0], types, summary, discoveredAt: message.ts, did: message.from || null, room: message.room, sourcePost: { text: message.text, ts: message.ts, nonce: message.nonce }, urls: extractUrls(message.text), deadline: extractSignal(message.text, DEADLINE_PATTERN), participationRequirements: extractSignal(message.text, REQUIREMENTS_PATTERN), duplicateCount: 1, verification: { status: suspicious ? "SUSPICIOUS" : "UNVERIFIED", confidence: suspicious ? "Low" : "Unverified", officialSource: null, checkedAt: null, note: suspicious ? "Contains a wallet, signing, or secret-related phrase. Do not act on it." : "Discovered from an unverified Technocore post. No external source was fetched." } }; }
function classifyOpportunityExact(message) { const types = OPPORTUNITY_RULES.filter(([, pattern]) => pattern.test(message.text)).map(([type]) => type); if (!types.length) return null; const explicitAction = /\b(?:new|join|register|claim now|apply|deadline|eligible|requirements?)\b/i.test(message.text); if (HEALTH_REPORT_PATTERN.test(message.text) && !explicitAction) return null; const fingerprint = normalizeCandidateText(message.text); if (!fingerprint) return null; const suspicious = SUSPICIOUS_PATTERN.test(message.text); return { id: sha256Hex(fingerprint).slice(0, 16), type: types[0], types, summary: message.text.slice(0, 500), discoveredAt: message.ts, did: DID_PATTERN.test(message.from) ? message.from : null, room: message.room, sourcePost: message, urls: extractUrls(message.text), deadline: extractSignal(message.text, DEADLINE_PATTERN), participationRequirements: extractSignal(message.text, REQUIREMENTS_PATTERN), duplicateCount: 1, verification: { status: suspicious ? "SUSPICIOUS" : "UNVERIFIED", confidence: suspicious ? "Low" : "Unverified", officialSource: null, checkedAt: null, note: suspicious ? "Contains a wallet, signing, or secret-related phrase. Do not act on it." : "Discovered from an unverified Technocore post. No external source was fetched." } }; }
function extractOpportunities(messages) { const candidates = new Map(); for (const message of messages) { const candidate = classifyOpportunityExact(message); if (!candidate) continue; const existing = candidates.get(candidate.id); if (!existing) candidates.set(candidate.id, candidate); else if (timestamp(candidate.discoveredAt) > timestamp(existing.discoveredAt)) candidates.set(candidate.id, { ...candidate, duplicateCount: existing.duplicateCount + 1 }); else existing.duplicateCount += 1; } return [...candidates.values()].sort((a, b) => timestamp(b.discoveredAt) - timestamp(a.discoveredAt)); }
async function fetchUpstream(path, accept = "application/json") { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 12_000); try { const response = await fetch(`${origin}${path}`, { headers: { accept }, signal: controller.signal }); if (!response.ok) throw new Error(`Technocore returned ${response.status} for ${path}`); return accept === "application/json" ? response.json() : response.text(); } finally { clearTimeout(timer); } }
function buildSnapshot(overview, eventText, roomResults) { const collectedAt = Date.now(); const rooms = overview.map((room, index) => calculateMetrics(room, roomResults[index] ?? [], collectedAt)); const messages = rooms.flatMap((room) => room.messages).sort(compareNewest); const dids = new Map(); for (const message of messages) { if (!DID_PATTERN.test(message.from)) continue; const did = dids.get(message.from) ?? { did: message.from, firstSeen: message.ts, lastSeen: message.ts, activityCount: 0, rooms: new Set(), posts: [], contributions: [] }; did.activityCount += 1; did.rooms.add(message.room); did.posts.push(message); if (timestamp(message.ts) < timestamp(did.firstSeen)) did.firstSeen = message.ts; if (timestamp(message.ts) > timestamp(did.lastSeen)) did.lastSeen = message.ts; if (CONTRIBUTION_PATTERN.test(message.text)) did.contributions.push(message); dids.set(message.from, did); } const didList = [...dids.values()].map((did) => ({ ...did, rooms: [...did.rooms], posts: did.posts.sort(compareNewest).slice(0, 50), contributions: did.contributions.sort(compareNewest).slice(0, 50) })).sort((a, b) => timestamp(b.lastSeen) - timestamp(a.lastSeen)); return { collectedAt: new Date(collectedAt).toISOString(), source: origin, roomTotal: number(overview.total), rooms, messages, dids: didList, contributions: messages.filter((message) => CONTRIBUTION_PATTERN.test(message.text)).slice(0, 100), opportunities: extractOpportunities(messages), newRooms: parseRoomEvents(eventText), memeCounts: messages.reduce((count, message) => ({ nullpo: count.nullpo + (message.text.match(/ぬるぽ/g)?.length ?? 0), ga: count.ga + (message.text.match(/ガッ|ガッ/g)?.length ?? 0) }), { nullpo: 0, ga: 0 }) }; }
async function refreshSnapshot() { const [overviewPayload, eventText, twoChPayload] = await Promise.all([fetchUpstream(`/rooms?format=json&limit=${roomLimit}`), fetchUpstream("/r/events", "text/plain"), fetchUpstream("/r/2ch?format=json&limit=200")]); const overview = (Array.isArray(overviewPayload.rooms) ? overviewPayload.rooms : []).map(normalizeRoom).filter(Boolean); const twoChMessages = (Array.isArray(twoChPayload.messages) ? twoChPayload.messages : []).map((message) => normalizeMessage(message, "2ch")).filter(Boolean); if (!overview.some((room) => room.room === "2ch")) overview.push({ room: "2ch", lastSeq: number(twoChPayload.last_seq), bytes: 0, idleSeconds: 0, topic: "", window: 0, zeroResponseShare: null, nickDiversity: null }); const roomResults = await mapConcurrent(overview, async (room) => { if (room.room === "2ch") return twoChMessages; try { const payload = await fetchUpstream(`/r/${encodeURIComponent(room.room)}?format=json&limit=200`); return (Array.isArray(payload.messages) ? payload.messages : []).map((message) => normalizeMessage(message, room.room)).filter(Boolean); } catch (cause) { log.warn?.(`Room fetch failed for ${room.room}: ${cause.message}`); return []; } }); return buildSnapshot(overview, eventText, roomResults); }
async function getSnapshot() { if (cache.value && Date.now() < cache.expiresAt) return cache.value; if (!cache.refreshPromise) cache.refreshPromise = refreshSnapshot().then((value) => (cache = { value, expiresAt: Date.now() + cacheTtlMs, refreshPromise: null }, value)).catch((cause) => { cache.refreshPromise = null; throw cause; }); return cache.refreshPromise; }
async function ranked(snapshot) { const rooms = [...snapshot.rooms]; return { updatedAt: snapshot.collectedAt, source: snapshot.source, sampledRooms: rooms.length, totalRooms: snapshot.roomTotal, trending: rooms.filter((room) => room.sampledPostCount).sort((a, b) => b.velocity - a.velocity).slice(0, 12).map(publicRoom), fastestGrowing: rooms.filter((room) => room.sampledPostCount).sort((a, b) => b.growth - a.growth || b.posts1h - a.posts1h).slice(0, 12).map(publicRoom), topByPosts: rooms.sort((a, b) => b.sampledPostCount - a.sampledPostCount).slice(0, 12).map(publicRoom), special2ch: snapshot.rooms.find((room) => room.room === "2ch") ? publicRoom(snapshot.rooms.find((room) => room.room === "2ch")) : null, newRooms: snapshot.newRooms.slice(0, 12), activeAgents: snapshot.dids.slice(0, 12).map(publicDid), contributions: snapshot.contributions.slice(0, 12), memeCounts: snapshot.memeCounts, opportunities: (await withFeedback(snapshot.opportunities)).slice(0, 12), crowdPosts: snapshot.messages.slice(0, 120) }; }
async function dailyDigest(snapshot) { const opportunities = (await withFeedback(snapshot.opportunities)).slice(0, 8), trending = [...snapshot.rooms].sort((a, b) => b.velocity - a.velocity).slice(0, 8).map(publicRoom); const markdown = ["# Technocore Matome Daily Digest", `Generated: ${snapshot.collectedAt}`, "", "## Trending rooms", ...trending.map((room, index) => `${index + 1}. #${room.room} — velocity ${room.velocity}, ${room.posts1h} posts/hour${room.topic ? ` — ${room.topic}` : ""}`), "", "## Unverified opportunities", ...opportunities.map((opportunity) => `- [${opportunity.verification.status}] ${opportunity.type}: ${opportunity.summary} (#${opportunity.room})`), "", "Independent observer for Technocore. Not affiliated with FLOP Labs or Technocore. This digest contains untrusted public content."].join("\n"); return { generatedAt: snapshot.collectedAt, operatorDid, markdown, trending, opportunities, recordingPackage: { mode: "manual-signing-only", operatorDid, room: "technocore", instructions: "Review the digest, host it at a public URL, then sign and publish with your existing local DID client. Do not send a private key to Technocore Matome.", text: `Technocore Matome Daily Digest (${snapshot.collectedAt}): ${trending.slice(0, 3).map((room) => `#${room.room} v${room.velocity}`).join(" | ")}.` } }; }
return { async handle(request) { const url = new URL(request.url); try { if (request.method === "POST" && url.pathname === "/api/feedback") { const raw = await request.text(); if (raw.length > 1024) return error("Feedback payload too large.", 413); let payload; try { payload = JSON.parse(raw); } catch { return error("Invalid JSON request body.", 400); } if (!/^[a-f0-9]{16}$/.test(payload.targetId ?? "") || !REACTIONS.has(payload.reaction)) return error("Invalid feedback payload.", 400); const all = await loadFeedback(), current = { useful: 0, noise: 0, needs_verification: 0, interesting: 0, ...(all[payload.targetId] ?? {}) }; current[payload.reaction] += 1; all[payload.targetId] = current; await feedbackStore.save(all); return json({ targetId: payload.targetId, feedback: current, privacy: "Aggregate counts only; no DID, IP, or message body stored." }, 201); } if (request.method !== "GET") return error("GET only, except explicit aggregate feedback.", 405); if (!url.pathname.startsWith("/api/")) return error("Unknown API route."); const snapshot = await getSnapshot(); if (url.pathname === "/api/trending") return json(await ranked(snapshot)); if (url.pathname === "/api/rooms") return json({ updatedAt: snapshot.collectedAt, rooms: snapshot.rooms.map(publicRoom) }); if (url.pathname === "/api/contributions") return json({ updatedAt: snapshot.collectedAt, classifier: "Keyword matched within sampled posts; not verified as a formal contribution.", contributions: snapshot.contributions }); if (url.pathname === "/api/digest/daily") return json(await dailyDigest(snapshot)); if (url.pathname === "/api/technoscope/contributions") return json({ operatorDid, history: [], recordingMode: "Manual signing only. Technocore Matome never receives or stores private keys." }); if (url.pathname === "/api/opportunities") return json({ updatedAt: snapshot.collectedAt, verificationPolicy: "Every candidate is unverified unless a future explicit verification pipeline records an external source.", opportunities: snapshot.opportunities }); const recordMatch = /^\/api\/opportunities\/([a-f0-9]{16})\/record$/.exec(url.pathname); if (recordMatch) { const item = snapshot.opportunities.find((entry) => entry.id === recordMatch[1]); return item ? json({ mode: "manual-signing-only", instructions: "Review this discovery and use your local DID client to sign the text. Technocore Matome never receives a private key.", text: `I discovered an unverified ${item.type} candidate via Technocore Matome in #${item.room} at ${item.discoveredAt}. Source post: ${item.summary}`, source: { room: item.room, did: item.did, discoveredAt: item.discoveredAt } }) : error("Opportunity not present in current cached sample."); } const opportunityMatch = /^\/api\/opportunities\/([a-f0-9]{16})$/.exec(url.pathname); if (opportunityMatch) { const opportunity = snapshot.opportunities.find((entry) => entry.id === opportunityMatch[1]); return opportunity ? json({ updatedAt: snapshot.collectedAt, opportunity }) : error("Opportunity not present in current cached sample."); } const roomMatch = /^\/api\/rooms\/([a-z0-9][a-z0-9_-]{0,47})$/.exec(url.pathname); if (roomMatch) { const room = snapshot.rooms.find((entry) => entry.room === roomMatch[1]); return room ? json({ updatedAt: snapshot.collectedAt, room, technocoreUrl: `${origin}/r/${encodeURIComponent(room.room)}` }) : error("Room not present in current sample."); } const didMatch = /^\/api\/did\/(did%3Akey%3A[A-Za-z0-9_-]{20,200})$/i.exec(url.pathname); if (didMatch) { const did = decodeURIComponent(didMatch[1]), record = snapshot.dids.find((entry) => entry.did === did); return record ? json({ updatedAt: snapshot.collectedAt, did: publicDid(record), scope: "Observed within cached room samples only." }) : error("DID not observed in current cached sample."); } return error("Unknown API route."); } catch (cause) { log.error?.(cause); return error("Technocore data could not be refreshed. Please retry shortly.", 502); } } };
}