mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
deter
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BnYVDOhM.js"></script>
|
<script type="module" crossorigin src="/assets/index-cxDsY4fK.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-gFnxuq00.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-gFnxuq00.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const { v4: uuidv4 } = require('uuid');
|
|||||||
const io = require('../globals/io');
|
const io = require('../globals/io');
|
||||||
const roverManager = require('./roverManager');
|
const roverManager = require('./roverManager');
|
||||||
const { isAdmin, isLockdownAdmin } = require('./roleService');
|
const { isAdmin, isLockdownAdmin } = require('./roleService');
|
||||||
|
const { isDeterred } = require('./verificationService');
|
||||||
const logger = require('../globals/logger').child('commandService');
|
const logger = require('../globals/logger').child('commandService');
|
||||||
const { isNightVisionBlocked } = require('../rewards/definitions/darkness');
|
const { isNightVisionBlocked } = require('../rewards/definitions/darkness');
|
||||||
|
|
||||||
@@ -80,6 +81,9 @@ io.on('connection', (socket) => {
|
|||||||
const isRebootCommand = type === 'reboot';
|
const isRebootCommand = type === 'reboot';
|
||||||
const isSongCommand = type === 'song' || (type === 'raw' && isSongRawPayload(payload));
|
const isSongCommand = type === 'song' || (type === 'raw' && isSongRawPayload(payload));
|
||||||
const isAdminSocket = isAdmin(socket);
|
const isAdminSocket = isAdmin(socket);
|
||||||
|
if (!isAdminSocket && isDeterred(socket)) {
|
||||||
|
throw new Error('Not authorized');
|
||||||
|
}
|
||||||
if (isRebootCommand && !isAdminSocket) {
|
if (isRebootCommand && !isAdminSocket) {
|
||||||
throw new Error('Not authorized');
|
throw new Error('Not authorized');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ const {
|
|||||||
denyRequest,
|
denyRequest,
|
||||||
listVerifiedUsers,
|
listVerifiedUsers,
|
||||||
removeVerifiedUser,
|
removeVerifiedUser,
|
||||||
|
listDeterredUsers,
|
||||||
|
deterUser,
|
||||||
|
undeterUser,
|
||||||
} = require('./verificationService');
|
} = require('./verificationService');
|
||||||
const {
|
const {
|
||||||
DM_APPROVE_EMOJI: PRIVATE_ACCESS_APPROVE_EMOJI,
|
DM_APPROVE_EMOJI: PRIVATE_ACCESS_APPROVE_EMOJI,
|
||||||
@@ -295,6 +298,9 @@ function formatHelp() {
|
|||||||
'`rs goal [text|clear]` — show or set community goal',
|
'`rs goal [text|clear]` — show or set community goal',
|
||||||
'`rs verify list` — list verified users (lockdown admins)',
|
'`rs verify list` — list verified users (lockdown admins)',
|
||||||
'`rs verify remove <cookieUserId|nickname>` — remove verified user (lockdown admins)',
|
'`rs verify remove <cookieUserId|nickname>` — remove verified user (lockdown admins)',
|
||||||
|
'`rs deter list` — list deterred users (lockdown admins)',
|
||||||
|
'`rs deter ban <cookieUserId|nickname|ip> [reason]` — deter a user (lockdown admins)',
|
||||||
|
'`rs deter unban <id|cookieUserId|nickname|ip>` — remove deterrence (lockdown admins)',
|
||||||
'`ts` — show time status',
|
'`ts` — show time status',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
@@ -672,6 +678,98 @@ async function handleVerifyCommand(message, tokens) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeterCommand(message, tokens) {
|
||||||
|
if (!isLockdownAdminUser(message.author?.id)) {
|
||||||
|
await message.reply({
|
||||||
|
content: 'Only lockdown admins can manage deterred users.',
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = (tokens.shift() || 'list').toLowerCase();
|
||||||
|
|
||||||
|
if (action === 'list') {
|
||||||
|
const users = listDeterredUsers();
|
||||||
|
if (!users.length) {
|
||||||
|
await message.reply({
|
||||||
|
content: 'No deterred users.',
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = users.map((entry, idx) => {
|
||||||
|
const updated = entry.updatedAt ? new Date(entry.updatedAt).toLocaleString() : 'unknown';
|
||||||
|
const ipCount = Array.isArray(entry.knownIps) ? entry.knownIps.length : 0;
|
||||||
|
const reason = entry.reason ? ` | reason:${entry.reason}` : '';
|
||||||
|
return `${idx + 1}. ${entry.id} | ${entry.nickname || 'unknown'} | ${formatMaskedCookieKey(entry.cookieUserId)} | ips:${ipCount} | updated:${updated}${reason}`;
|
||||||
|
});
|
||||||
|
await message.reply({
|
||||||
|
content: ['Deterred users:', ...lines].join('\n').slice(0, 1900),
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'ban') {
|
||||||
|
const selector = String(tokens.shift() || '').trim();
|
||||||
|
const reason = tokens.join(' ').trim();
|
||||||
|
if (!selector) {
|
||||||
|
await message.reply({
|
||||||
|
content: 'Usage: `rs deter ban <cookieUserId|nickname|ip> [reason]`',
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const deterred = deterUser(selector, { reason, actor: message.author?.id || null });
|
||||||
|
await message.reply({
|
||||||
|
content: sanitizeMentions(
|
||||||
|
`${deterred.created ? 'Deterred' : 'Updated deterrence for'} ${deterred.nickname || 'unknown'} (${formatMaskedCookieKey(deterred.cookieUserId)}).`,
|
||||||
|
),
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
await message.reply({
|
||||||
|
content: sanitizeMentions(`Failed to deter user: ${err.message}`),
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'unban') {
|
||||||
|
const selector = tokens.join(' ').trim();
|
||||||
|
if (!selector) {
|
||||||
|
await message.reply({
|
||||||
|
content: 'Usage: `rs deter unban <id|cookieUserId|nickname|ip>`',
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const removed = undeterUser(selector, message.author?.id || null);
|
||||||
|
await message.reply({
|
||||||
|
content: sanitizeMentions(
|
||||||
|
`Removed deterrence for ${removed.nickname || 'unknown'} (${formatMaskedCookieKey(removed.cookieUserId)}).`,
|
||||||
|
),
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
await message.reply({
|
||||||
|
content: sanitizeMentions(`Failed to remove deterrence: ${err.message}`),
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await message.reply({
|
||||||
|
content: 'Unknown deter command. Use `rs deter list`, `rs deter ban <selector> [reason]`, or `rs deter unban <selector>`.',
|
||||||
|
allowedMentions: { parse: [], repliedUser: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function canManageBridge(message) {
|
function canManageBridge(message) {
|
||||||
if (isAdminUser(message.author.id)) return true;
|
if (isAdminUser(message.author.id)) return true;
|
||||||
if (!message.guild || !message.member) return false;
|
if (!message.guild || !message.member) return false;
|
||||||
@@ -849,7 +947,7 @@ async function handleCommand(message) {
|
|||||||
const isLockdownAdmin = isLockdownAdminUser(message.author.id);
|
const isLockdownAdmin = isLockdownAdminUser(message.author.id);
|
||||||
const isBridgeAdmin = action === 'bridge' ? canManageBridge(message) : false;
|
const isBridgeAdmin = action === 'bridge' ? canManageBridge(message) : false;
|
||||||
const mode = getMode();
|
const mode = getMode();
|
||||||
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify']);
|
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter']);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isAdmin &&
|
!isAdmin &&
|
||||||
@@ -861,7 +959,8 @@ async function handleCommand(message) {
|
|||||||
action !== 'bridge' &&
|
action !== 'bridge' &&
|
||||||
action !== 'goal' &&
|
action !== 'goal' &&
|
||||||
action !== 'reason' &&
|
action !== 'reason' &&
|
||||||
action !== 'verify'
|
action !== 'verify' &&
|
||||||
|
action !== 'deter'
|
||||||
) {
|
) {
|
||||||
return; // ignore non-admins for privileged commands
|
return; // ignore non-admins for privileged commands
|
||||||
}
|
}
|
||||||
@@ -908,6 +1007,9 @@ async function handleCommand(message) {
|
|||||||
case 'verify':
|
case 'verify':
|
||||||
await handleVerifyCommand(message, tokens);
|
await handleVerifyCommand(message, tokens);
|
||||||
break;
|
break;
|
||||||
|
case 'deter':
|
||||||
|
await handleDeterCommand(message, tokens);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
await message.reply(formatHelp());
|
await message.reply(formatHelp());
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { getSocketIp, normalizeIp } = require('../helpers/ipResolver');
|
||||||
|
|
||||||
|
const COOKIE_USER_ID_RE = /^cu_[a-f0-9]{32}$/;
|
||||||
|
|
||||||
|
function sanitizeNickname(raw) {
|
||||||
|
if (typeof raw !== 'string') return '';
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return '';
|
||||||
|
return trimmed.replace(/\*/g, 'nope').slice(0, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCookieUserId(value) {
|
||||||
|
const raw = typeof value === 'string' ? value.trim() : '';
|
||||||
|
if (!raw) return '';
|
||||||
|
return raw.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidCookieUserId(value) {
|
||||||
|
return COOKIE_USER_ID_RE.test(normalizeCookieUserId(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCookieUserId() {
|
||||||
|
return `cu_${crypto.randomBytes(16).toString('hex')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKnownIp(socket) {
|
||||||
|
return normalizeIp(getSocketIp(socket));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createJsonStore({ path, normalizeStoreShape, cloneStore, logger }) {
|
||||||
|
let cache = null;
|
||||||
|
|
||||||
|
function loadStore() {
|
||||||
|
if (cache) return cache;
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(path, 'utf8');
|
||||||
|
cache = normalizeStoreShape(JSON.parse(raw));
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
logger?.warn?.('Failed to load JSON store', { path, error: err.message });
|
||||||
|
}
|
||||||
|
cache = normalizeStoreShape({});
|
||||||
|
}
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStore(next) {
|
||||||
|
const normalized = normalizeStoreShape(next);
|
||||||
|
fs.mkdirSync(require('path').dirname(path), { recursive: true });
|
||||||
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
||||||
|
fs.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
||||||
|
fs.renameSync(tempPath, path);
|
||||||
|
cache = normalized;
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withStore(mutator) {
|
||||||
|
const current = loadStore();
|
||||||
|
const draft = cloneStore(current);
|
||||||
|
const result = mutator(draft);
|
||||||
|
writeStore(draft);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadStore,
|
||||||
|
writeStore,
|
||||||
|
withStore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sanitizeNickname,
|
||||||
|
normalizeCookieUserId,
|
||||||
|
isValidCookieUserId,
|
||||||
|
generateCookieUserId,
|
||||||
|
getKnownIp,
|
||||||
|
createJsonStore,
|
||||||
|
};
|
||||||
@@ -186,6 +186,10 @@ function getControlDenialReason(record, socket, options = {}) {
|
|||||||
if (mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
if (mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||||
return 'Server in lockdown';
|
return 'Server in lockdown';
|
||||||
}
|
}
|
||||||
|
const { isDeterred } = require('./verificationService');
|
||||||
|
if (!isAdmin(socket) && isDeterred(socket)) {
|
||||||
|
return 'Not authorized';
|
||||||
|
}
|
||||||
if (!isPrivateRecord(record)) {
|
if (!isPrivateRecord(record)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const { getState: getHomeAssistantState, homeAssistantEvents } = require('./home
|
|||||||
const { getNickname, nicknameEvents } = require('./nicknameService');
|
const { getNickname, nicknameEvents } = require('./nicknameService');
|
||||||
const {
|
const {
|
||||||
getVerificationStateForSocket,
|
getVerificationStateForSocket,
|
||||||
|
getModerationStateForSocket,
|
||||||
getIdentitySummary,
|
getIdentitySummary,
|
||||||
verificationEvents,
|
verificationEvents,
|
||||||
} = require('./verificationService');
|
} = require('./verificationService');
|
||||||
@@ -133,6 +134,7 @@ function buildSession(socket) {
|
|||||||
},
|
},
|
||||||
identity: getIdentitySummary(socket),
|
identity: getIdentitySummary(socket),
|
||||||
verification: getVerificationStateForSocket(socket),
|
verification: getVerificationStateForSocket(socket),
|
||||||
|
moderation: getModerationStateForSocket(socket),
|
||||||
privateRoverAccess: getPrivateRoverAccessStateForSocket(socket),
|
privateRoverAccess: getPrivateRoverAccessStateForSocket(socket),
|
||||||
isVerified: Boolean(socket?.data?.isVerified),
|
isVerified: Boolean(socket?.data?.isVerified),
|
||||||
audioForward: getAudioForwardState(),
|
audioForward: getAudioForwardState(),
|
||||||
|
|||||||
@@ -1,97 +1,85 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const net = require('net');
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const io = require('../globals/io');
|
const io = require('../globals/io');
|
||||||
const logger = require('../globals/logger').child('verificationService');
|
const logger = require('../globals/logger').child('verificationService');
|
||||||
const { publishEvent } = require('./eventBus');
|
const { publishEvent } = require('./eventBus');
|
||||||
const { getSocketIp, normalizeIp } = require('../helpers/ipResolver');
|
const { normalizeIp } = require('../helpers/ipResolver');
|
||||||
const { getNickname, setNickname } = require('./nicknameService');
|
const { getNickname, setNickname } = require('./nicknameService');
|
||||||
const { getRole, roleEvents } = require('./roleService');
|
const { getRole, roleEvents } = require('./roleService');
|
||||||
|
const {
|
||||||
|
sanitizeNickname,
|
||||||
|
normalizeCookieUserId,
|
||||||
|
isValidCookieUserId,
|
||||||
|
generateCookieUserId,
|
||||||
|
getKnownIp,
|
||||||
|
createJsonStore,
|
||||||
|
} = require('./identityService');
|
||||||
|
|
||||||
const DATA_DIR = path.join(__dirname, '..', '..', 'data');
|
const DATA_DIR = path.join(__dirname, '..', '..', 'data');
|
||||||
const STORE_PATH = path.join(DATA_DIR, 'verified-users.json');
|
const STORE_PATH = path.join(DATA_DIR, 'verified-users.json');
|
||||||
const COOKIE_USER_ID_RE = /^cu_[a-f0-9]{32}$/;
|
|
||||||
|
|
||||||
const verificationEvents = new EventEmitter();
|
const verificationEvents = new EventEmitter();
|
||||||
|
|
||||||
let cache = null;
|
|
||||||
|
|
||||||
function sanitizeNickname(raw) {
|
|
||||||
if (typeof raw !== 'string') return '';
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
if (!trimmed) return '';
|
|
||||||
return trimmed.replace(/\*/g, 'nope').slice(0, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeStoreShape(store) {
|
function normalizeStoreShape(store) {
|
||||||
const next = store && typeof store === 'object' ? store : {};
|
const next = store && typeof store === 'object' ? store : {};
|
||||||
return {
|
return {
|
||||||
verifiedUsers: Array.isArray(next.verifiedUsers) ? next.verifiedUsers : [],
|
verifiedUsers: Array.isArray(next.verifiedUsers) ? next.verifiedUsers : [],
|
||||||
pendingRequests: Array.isArray(next.pendingRequests) ? next.pendingRequests : [],
|
pendingRequests: Array.isArray(next.pendingRequests) ? next.pendingRequests : [],
|
||||||
dmMessages: Array.isArray(next.dmMessages) ? next.dmMessages : [],
|
dmMessages: Array.isArray(next.dmMessages) ? next.dmMessages : [],
|
||||||
|
deterredUsers: Array.isArray(next.deterredUsers) ? next.deterredUsers : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadStore() {
|
function cloneStore(current) {
|
||||||
if (cache) return cache;
|
return {
|
||||||
try {
|
verifiedUsers: (current.verifiedUsers || []).map((entry) => ({ ...entry, knownIps: [...(entry.knownIps || [])] })),
|
||||||
const raw = fs.readFileSync(STORE_PATH, 'utf8');
|
pendingRequests: (current.pendingRequests || []).map((entry) => ({ ...entry })),
|
||||||
cache = normalizeStoreShape(JSON.parse(raw));
|
dmMessages: (current.dmMessages || []).map((entry) => ({ ...entry })),
|
||||||
} catch (err) {
|
deterredUsers: (current.deterredUsers || []).map((entry) => ({ ...entry, knownIps: [...(entry.knownIps || [])] })),
|
||||||
if (err.code !== 'ENOENT') {
|
|
||||||
logger.warn('Failed to load verification store', err.message);
|
|
||||||
}
|
|
||||||
cache = normalizeStoreShape({});
|
|
||||||
}
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeStore(next) {
|
|
||||||
const normalized = normalizeStoreShape(next);
|
|
||||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
||||||
const tempPath = `${STORE_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
||||||
fs.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
|
||||||
fs.renameSync(tempPath, STORE_PATH);
|
|
||||||
cache = normalized;
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function withStore(mutator) {
|
|
||||||
const current = loadStore();
|
|
||||||
const draft = {
|
|
||||||
verifiedUsers: current.verifiedUsers.map((entry) => ({ ...entry, knownIps: [...(entry.knownIps || [])] })),
|
|
||||||
pendingRequests: current.pendingRequests.map((entry) => ({ ...entry })),
|
|
||||||
dmMessages: current.dmMessages.map((entry) => ({ ...entry })),
|
|
||||||
};
|
};
|
||||||
const result = mutator(draft);
|
|
||||||
writeStore(draft);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateCookieUserId() {
|
const storeApi = createJsonStore({
|
||||||
return `cu_${crypto.randomBytes(16).toString('hex')}`;
|
path: STORE_PATH,
|
||||||
}
|
normalizeStoreShape,
|
||||||
|
cloneStore,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
function normalizeCookieUserId(value) {
|
const { loadStore, withStore } = storeApi;
|
||||||
const raw = typeof value === 'string' ? value.trim() : '';
|
|
||||||
if (!raw) return '';
|
|
||||||
return raw.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidCookieUserId(value) {
|
|
||||||
return COOKIE_USER_ID_RE.test(normalizeCookieUserId(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKnownIp(socket) {
|
|
||||||
return normalizeIp(getSocketIp(socket));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureSocketData(socket) {
|
function ensureSocketData(socket) {
|
||||||
socket.data = socket.data || {};
|
socket.data = socket.data || {};
|
||||||
return socket.data;
|
return socket.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function identityFromSocket(socket) {
|
||||||
|
const data = ensureSocketData(socket);
|
||||||
|
return {
|
||||||
|
cookieUserId: normalizeCookieUserId(data.cookieUserId),
|
||||||
|
nickname: sanitizeNickname(getNickname(socket)),
|
||||||
|
ip: getKnownIp(socket),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNicknameKey(value) {
|
||||||
|
return sanitizeNickname(value).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKnownIps(raw = []) {
|
||||||
|
const out = [];
|
||||||
|
(Array.isArray(raw) ? raw : []).forEach((value) => {
|
||||||
|
const ip = typeof value === 'string' ? value.trim() : '';
|
||||||
|
if (!ip) return;
|
||||||
|
if (!out.includes(ip)) {
|
||||||
|
out.push(ip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function findVerifiedMatch(store, { cookieUserId, ip }) {
|
function findVerifiedMatch(store, { cookieUserId, ip }) {
|
||||||
if (!cookieUserId && !ip) return null;
|
if (!cookieUserId && !ip) return null;
|
||||||
const byCookie = cookieUserId
|
const byCookie = cookieUserId
|
||||||
@@ -104,6 +92,25 @@ function findVerifiedMatch(store, { cookieUserId, ip }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAdminRole(role) {
|
||||||
|
return role === 'admin' || role === 'lockdown' || role === 'lockdown-admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDeterredMatch(store, { cookieUserId, nickname, ip }) {
|
||||||
|
const nicknameKey = normalizeNicknameKey(nickname);
|
||||||
|
return (
|
||||||
|
(store.deterredUsers || []).find((entry) => {
|
||||||
|
const entryCookie = normalizeCookieUserId(entry.cookieUserId);
|
||||||
|
if (cookieUserId && entryCookie && entryCookie === cookieUserId) return true;
|
||||||
|
const entryIps = normalizeKnownIps(entry.knownIps);
|
||||||
|
if (ip && entryIps.includes(ip)) return true;
|
||||||
|
const entryNicknameKey = normalizeNicknameKey(entry.nickname);
|
||||||
|
if (nicknameKey && entryNicknameKey && entryNicknameKey === nicknameKey) return true;
|
||||||
|
return false;
|
||||||
|
}) || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function emitChange(reason, payload = {}) {
|
function emitChange(reason, payload = {}) {
|
||||||
verificationEvents.emit('change', { reason, ...payload });
|
verificationEvents.emit('change', { reason, ...payload });
|
||||||
}
|
}
|
||||||
@@ -113,9 +120,7 @@ function reevaluateSocketVerification(socket) {
|
|||||||
const store = loadStore();
|
const store = loadStore();
|
||||||
const data = ensureSocketData(socket);
|
const data = ensureSocketData(socket);
|
||||||
const role = getRole(socket);
|
const role = getRole(socket);
|
||||||
const cookieUserId = normalizeCookieUserId(data.cookieUserId);
|
const { cookieUserId, nickname, ip } = identityFromSocket(socket);
|
||||||
const nickname = sanitizeNickname(getNickname(socket));
|
|
||||||
const ip = getKnownIp(socket);
|
|
||||||
|
|
||||||
if (role === 'lockdown') {
|
if (role === 'lockdown') {
|
||||||
data.isVerified = true;
|
data.isVerified = true;
|
||||||
@@ -167,6 +172,59 @@ function reevaluateSocketVerification(socket) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reevaluateSocketDeterrence(socket) {
|
||||||
|
if (!socket) return { isDeterred: false, matchedRecordId: null, reason: 'missing_socket' };
|
||||||
|
const store = loadStore();
|
||||||
|
const data = ensureSocketData(socket);
|
||||||
|
const role = getRole(socket);
|
||||||
|
const { cookieUserId, nickname, ip } = identityFromSocket(socket);
|
||||||
|
|
||||||
|
if (isAdminRole(role)) {
|
||||||
|
data.isDeterred = false;
|
||||||
|
data.deterredRecordId = null;
|
||||||
|
return {
|
||||||
|
isDeterred: false,
|
||||||
|
matchedRecordId: null,
|
||||||
|
reason: 'admin_bypass',
|
||||||
|
cookieUserId,
|
||||||
|
nickname,
|
||||||
|
ip,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = findDeterredMatch(store, { cookieUserId, nickname, ip });
|
||||||
|
const isDeterred = Boolean(match);
|
||||||
|
data.isDeterred = isDeterred;
|
||||||
|
data.deterredRecordId = isDeterred ? match.id : null;
|
||||||
|
|
||||||
|
if (isDeterred) {
|
||||||
|
withStore((draft) => {
|
||||||
|
const record = (draft.deterredUsers || []).find((entry) => entry.id === match.id);
|
||||||
|
if (!record) return;
|
||||||
|
record.updatedAt = Date.now();
|
||||||
|
if (cookieUserId) {
|
||||||
|
record.cookieUserId = cookieUserId;
|
||||||
|
}
|
||||||
|
if (nickname) {
|
||||||
|
record.nickname = nickname;
|
||||||
|
}
|
||||||
|
record.knownIps = normalizeKnownIps(record.knownIps);
|
||||||
|
if (ip && !record.knownIps.includes(ip)) {
|
||||||
|
record.knownIps.push(ip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDeterred,
|
||||||
|
matchedRecordId: isDeterred ? match.id : null,
|
||||||
|
reason: isDeterred ? 'matched' : 'no_match',
|
||||||
|
cookieUserId,
|
||||||
|
nickname,
|
||||||
|
ip,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function identifySocket(socket, payload = {}) {
|
function identifySocket(socket, payload = {}) {
|
||||||
if (!socket) {
|
if (!socket) {
|
||||||
throw new Error('Socket required');
|
throw new Error('Socket required');
|
||||||
@@ -192,10 +250,12 @@ function identifySocket(socket, payload = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const verification = reevaluateSocketVerification(socket);
|
const verification = reevaluateSocketVerification(socket);
|
||||||
|
const deterrence = reevaluateSocketDeterrence(socket);
|
||||||
emitChange('identify', { socketId: socket.id });
|
emitChange('identify', { socketId: socket.id });
|
||||||
return {
|
return {
|
||||||
cookieUserId: data.cookieUserId,
|
cookieUserId: data.cookieUserId,
|
||||||
isVerified: verification.isVerified,
|
isVerified: verification.isVerified,
|
||||||
|
isDeterred: deterrence.isDeterred,
|
||||||
reason: verification.reason,
|
reason: verification.reason,
|
||||||
identifiedAt: Date.now(),
|
identifiedAt: Date.now(),
|
||||||
};
|
};
|
||||||
@@ -267,6 +327,7 @@ function removeVerifiedUser(selector, removedBy = null) {
|
|||||||
const data = ensureSocketData(socket);
|
const data = ensureSocketData(socket);
|
||||||
if (normalizeCookieUserId(data.cookieUserId) === removed.cookieUserId) {
|
if (normalizeCookieUserId(data.cookieUserId) === removed.cookieUserId) {
|
||||||
reevaluateSocketVerification(socket);
|
reevaluateSocketVerification(socket);
|
||||||
|
reevaluateSocketDeterrence(socket);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,9 +351,7 @@ function createVerificationRequest(socket) {
|
|||||||
throw new Error('Socket required');
|
throw new Error('Socket required');
|
||||||
}
|
}
|
||||||
const data = ensureSocketData(socket);
|
const data = ensureSocketData(socket);
|
||||||
const cookieUserId = normalizeCookieUserId(data.cookieUserId);
|
const { cookieUserId, nickname, ip } = identityFromSocket(socket);
|
||||||
const nickname = sanitizeNickname(getNickname(socket));
|
|
||||||
const ip = getKnownIp(socket);
|
|
||||||
|
|
||||||
if (data.isVerified) {
|
if (data.isVerified) {
|
||||||
throw new Error('You are already verified.');
|
throw new Error('You are already verified.');
|
||||||
@@ -413,6 +472,7 @@ function approveRequest(requestId, actorDiscordId) {
|
|||||||
const data = ensureSocketData(socket);
|
const data = ensureSocketData(socket);
|
||||||
if (normalizeCookieUserId(data.cookieUserId) === request.cookieUserId) {
|
if (normalizeCookieUserId(data.cookieUserId) === request.cookieUserId) {
|
||||||
reevaluateSocketVerification(socket);
|
reevaluateSocketVerification(socket);
|
||||||
|
reevaluateSocketDeterrence(socket);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -476,10 +536,211 @@ function getVerificationStateForSocket(socket) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getModerationStateForSocket(socket) {
|
||||||
|
const data = socket?.data || {};
|
||||||
|
return {
|
||||||
|
isDeterred: Boolean(data.isDeterred),
|
||||||
|
recordId: data.deterredRecordId || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function isVerified(socket) {
|
function isVerified(socket) {
|
||||||
return Boolean(socket?.data?.isVerified);
|
return Boolean(socket?.data?.isVerified);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDeterred(socket) {
|
||||||
|
return Boolean(socket?.data?.isDeterred);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDeterrenceSelector(selector) {
|
||||||
|
const value = String(selector || '').trim();
|
||||||
|
if (!value) {
|
||||||
|
throw new Error('Selector required.');
|
||||||
|
}
|
||||||
|
const cookie = normalizeCookieUserId(value);
|
||||||
|
if (cookie && isValidCookieUserId(cookie)) {
|
||||||
|
return { cookieUserId: cookie, nickname: '', ip: null };
|
||||||
|
}
|
||||||
|
const ip = normalizeIp(value);
|
||||||
|
if (ip && net.isIP(ip)) {
|
||||||
|
return { cookieUserId: '', nickname: '', ip };
|
||||||
|
}
|
||||||
|
const nickname = sanitizeNickname(value);
|
||||||
|
if (!nickname) {
|
||||||
|
throw new Error('Selector required.');
|
||||||
|
}
|
||||||
|
return { cookieUserId: '', nickname, ip: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeResolveVerifiedRecordForSelector(store, parsed) {
|
||||||
|
if (parsed.cookieUserId) {
|
||||||
|
return store.verifiedUsers.find((entry) => normalizeCookieUserId(entry.cookieUserId) === parsed.cookieUserId) || null;
|
||||||
|
}
|
||||||
|
if (parsed.ip) {
|
||||||
|
return store.verifiedUsers.find((entry) => Array.isArray(entry.knownIps) && entry.knownIps.includes(parsed.ip)) || null;
|
||||||
|
}
|
||||||
|
const byNickname = store.verifiedUsers.filter((entry) => normalizeNicknameKey(entry.nickname) === normalizeNicknameKey(parsed.nickname));
|
||||||
|
if (byNickname.length === 1) return byNickname[0];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listDeterredUsers() {
|
||||||
|
const store = loadStore();
|
||||||
|
return (store.deterredUsers || []).map((entry) => ({ ...entry, knownIps: [...(entry.knownIps || [])] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deterUser(selector, options = {}) {
|
||||||
|
const parsed = parseDeterrenceSelector(selector);
|
||||||
|
const reasonRaw = String(options?.reason || '').trim();
|
||||||
|
const reason = reasonRaw ? reasonRaw.slice(0, 240) : null;
|
||||||
|
const actor = options?.actor ? String(options.actor) : null;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
withStore((draft) => {
|
||||||
|
const verified = maybeResolveVerifiedRecordForSelector(draft, parsed);
|
||||||
|
const cookieUserId = parsed.cookieUserId || normalizeCookieUserId(verified?.cookieUserId || '');
|
||||||
|
const nickname = parsed.nickname || sanitizeNickname(verified?.nickname || '');
|
||||||
|
const knownIps = normalizeKnownIps([
|
||||||
|
...(parsed.ip ? [parsed.ip] : []),
|
||||||
|
...((verified && Array.isArray(verified.knownIps)) ? verified.knownIps : []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let existing = findDeterredMatch(draft, { cookieUserId, nickname, ip: parsed.ip || null });
|
||||||
|
if (!existing && cookieUserId) {
|
||||||
|
existing = (draft.deterredUsers || []).find((entry) => normalizeCookieUserId(entry.cookieUserId) === cookieUserId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (cookieUserId) {
|
||||||
|
existing.cookieUserId = cookieUserId;
|
||||||
|
}
|
||||||
|
if (nickname) {
|
||||||
|
existing.nickname = nickname;
|
||||||
|
}
|
||||||
|
const mergedIps = normalizeKnownIps([...(existing.knownIps || []), ...knownIps]);
|
||||||
|
existing.knownIps = mergedIps;
|
||||||
|
if (reason) {
|
||||||
|
existing.reason = reason;
|
||||||
|
}
|
||||||
|
existing.updatedAt = now;
|
||||||
|
existing.updatedBy = actor;
|
||||||
|
result = { ...existing, knownIps: [...(existing.knownIps || [])], created: false };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = {
|
||||||
|
id: `du_${crypto.randomBytes(8).toString('hex')}`,
|
||||||
|
cookieUserId: cookieUserId || null,
|
||||||
|
nickname: nickname || null,
|
||||||
|
knownIps,
|
||||||
|
reason,
|
||||||
|
createdAt: now,
|
||||||
|
createdBy: actor,
|
||||||
|
updatedAt: now,
|
||||||
|
updatedBy: actor,
|
||||||
|
};
|
||||||
|
draft.deterredUsers.push(created);
|
||||||
|
result = { ...created, knownIps: [...(created.knownIps || [])], created: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
io.sockets.sockets.forEach((socket) => {
|
||||||
|
reevaluateSocketDeterrence(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitChange('deter_update');
|
||||||
|
publishEvent({
|
||||||
|
source: 'moderation',
|
||||||
|
type: result?.created ? 'moderation.deterred' : 'moderation.deterrenceUpdated',
|
||||||
|
payload: {
|
||||||
|
id: result?.id || null,
|
||||||
|
cookieUserId: result?.cookieUserId || null,
|
||||||
|
nickname: result?.nickname || null,
|
||||||
|
knownIps: result?.knownIps || [],
|
||||||
|
reason: result?.reason || null,
|
||||||
|
actor,
|
||||||
|
ts: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDeterredSelector(selector) {
|
||||||
|
const store = loadStore();
|
||||||
|
const value = String(selector || '').trim();
|
||||||
|
if (!value) return { error: 'selector_required' };
|
||||||
|
|
||||||
|
const byId = (store.deterredUsers || []).find((entry) => String(entry.id) === value) || null;
|
||||||
|
if (byId) return { record: byId };
|
||||||
|
|
||||||
|
const cookie = normalizeCookieUserId(value);
|
||||||
|
if (cookie && isValidCookieUserId(cookie)) {
|
||||||
|
const byCookie = (store.deterredUsers || []).find((entry) => normalizeCookieUserId(entry.cookieUserId) === cookie) || null;
|
||||||
|
if (byCookie) return { record: byCookie };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = typeof value === 'string' ? value.trim() : '';
|
||||||
|
if (ip && net.isIP(ip)) {
|
||||||
|
const byIp = (store.deterredUsers || []).find((entry) => Array.isArray(entry.knownIps) && entry.knownIps.includes(ip)) || null;
|
||||||
|
if (byIp) return { record: byIp };
|
||||||
|
}
|
||||||
|
|
||||||
|
const nicknameKey = normalizeNicknameKey(value);
|
||||||
|
const byNickname = (store.deterredUsers || []).filter((entry) => normalizeNicknameKey(entry.nickname) === nicknameKey);
|
||||||
|
if (byNickname.length === 1) return { record: byNickname[0] };
|
||||||
|
if (byNickname.length > 1) return { error: 'ambiguous_nickname' };
|
||||||
|
|
||||||
|
return { error: 'not_found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function undeterUser(selector, removedBy = null) {
|
||||||
|
const resolved = resolveDeterredSelector(selector);
|
||||||
|
if (resolved.error) {
|
||||||
|
throw new Error(
|
||||||
|
resolved.error === 'ambiguous_nickname'
|
||||||
|
? 'Nickname matches multiple deterred users; remove by id or cookieUserId.'
|
||||||
|
: 'Deterred user not found.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = resolved.record;
|
||||||
|
let removed = null;
|
||||||
|
|
||||||
|
withStore((draft) => {
|
||||||
|
const before = draft.deterredUsers.length;
|
||||||
|
draft.deterredUsers = draft.deterredUsers.filter((entry) => entry.id !== target.id);
|
||||||
|
if (draft.deterredUsers.length !== before) {
|
||||||
|
removed = target;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!removed) {
|
||||||
|
throw new Error('Deterred user not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
io.sockets.sockets.forEach((socket) => {
|
||||||
|
reevaluateSocketDeterrence(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
const removedAt = Date.now();
|
||||||
|
emitChange('deter_remove');
|
||||||
|
publishEvent({
|
||||||
|
source: 'moderation',
|
||||||
|
type: 'moderation.undeterred',
|
||||||
|
payload: {
|
||||||
|
id: removed.id,
|
||||||
|
cookieUserId: removed.cookieUserId || null,
|
||||||
|
nickname: removed.nickname || null,
|
||||||
|
removedBy: removedBy ? String(removedBy) : null,
|
||||||
|
removedAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...removed, knownIps: [...(removed.knownIps || [])] };
|
||||||
|
}
|
||||||
|
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
identifySocket(socket, {});
|
identifySocket(socket, {});
|
||||||
|
|
||||||
@@ -506,6 +767,7 @@ roleEvents.on('change', ({ socket }) => {
|
|||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
try {
|
try {
|
||||||
reevaluateSocketVerification(socket);
|
reevaluateSocketVerification(socket);
|
||||||
|
reevaluateSocketDeterrence(socket);
|
||||||
emitChange('role_change', { socketId: socket.id });
|
emitChange('role_change', { socketId: socket.id });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('Failed to reevaluate verification on role change', err.message);
|
logger.warn('Failed to reevaluate verification on role change', err.message);
|
||||||
@@ -517,6 +779,7 @@ module.exports = {
|
|||||||
getVerificationStatus,
|
getVerificationStatus,
|
||||||
getIdentitySummary,
|
getIdentitySummary,
|
||||||
getVerificationStateForSocket,
|
getVerificationStateForSocket,
|
||||||
|
getModerationStateForSocket,
|
||||||
createVerificationRequest,
|
createVerificationRequest,
|
||||||
attachDmMessage,
|
attachDmMessage,
|
||||||
getRequestByMessageId,
|
getRequestByMessageId,
|
||||||
@@ -524,7 +787,12 @@ module.exports = {
|
|||||||
denyRequest,
|
denyRequest,
|
||||||
listVerifiedUsers,
|
listVerifiedUsers,
|
||||||
removeVerifiedUser,
|
removeVerifiedUser,
|
||||||
|
listDeterredUsers,
|
||||||
|
deterUser,
|
||||||
|
undeterUser,
|
||||||
isVerified,
|
isVerified,
|
||||||
|
isDeterred,
|
||||||
reevaluateSocketVerification,
|
reevaluateSocketVerification,
|
||||||
|
reevaluateSocketDeterrence,
|
||||||
verificationEvents,
|
verificationEvents,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useSessionSelector } from '../context/SessionContext.jsx';
|
||||||
|
|
||||||
|
function getRandomElement(elements) {
|
||||||
|
if (!elements || !elements.length) return null;
|
||||||
|
const idx = Math.floor(Math.random() * elements.length);
|
||||||
|
return elements[idx] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function corruptDomOnce() {
|
||||||
|
if (typeof document === 'undefined' || !document.body) return;
|
||||||
|
const elements = document.body.getElementsByTagName('*');
|
||||||
|
if (!elements || elements.length < 2) return;
|
||||||
|
|
||||||
|
const sourceA = getRandomElement(elements);
|
||||||
|
const targetA = getRandomElement(elements);
|
||||||
|
const classA = sourceA?.classList?.[0] || null;
|
||||||
|
if (classA && targetA?.classList) {
|
||||||
|
try {
|
||||||
|
targetA.classList.add(classA);
|
||||||
|
} catch {
|
||||||
|
// Best-effort chaos; ignore invalid class token failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceB = getRandomElement(elements);
|
||||||
|
const targetB = getRandomElement(elements);
|
||||||
|
const classB = sourceB?.classList?.[1] || null;
|
||||||
|
if (classB && targetB?.classList) {
|
||||||
|
try {
|
||||||
|
targetB.classList.add(classB);
|
||||||
|
} catch {
|
||||||
|
// Best-effort chaos; ignore invalid class token failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeterrenceChaos() {
|
||||||
|
const isDeterred = useSessionSelector((state) => Boolean(state.session?.moderation?.isDeterred));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDeterred) return undefined;
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
corruptDomOnce();
|
||||||
|
}, 1);
|
||||||
|
return () => {
|
||||||
|
window.clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [isDeterred]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { ChatProvider } from './context/ChatContext.jsx'
|
|||||||
import SpectatorApp from './spectate/SpectatorApp.jsx'
|
import SpectatorApp from './spectate/SpectatorApp.jsx'
|
||||||
import MiniSummaryApp from './mini/MiniSummaryApp.jsx'
|
import MiniSummaryApp from './mini/MiniSummaryApp.jsx'
|
||||||
import { SettingsProvider } from './settings/index.js'
|
import { SettingsProvider } from './settings/index.js'
|
||||||
|
import DeterrenceChaos from './components/DeterrenceChaos.jsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
@@ -18,6 +19,7 @@ createRoot(document.getElementById('root')).render(
|
|||||||
<TelemetryProvider>
|
<TelemetryProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<ChatProvider>
|
<ChatProvider>
|
||||||
|
<DeterrenceChaos />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<App />} />
|
<Route path="/" element={<App />} />
|
||||||
|
|||||||
Reference in New Issue
Block a user