feat(Chat): implement chat incident reporting feature
All checks were successful
Deploy to production / deploy (push) Successful in 1m57s

- Added reportChatIncident method in ChatController to handle reporting of chat incidents.
- Introduced a new API route for reporting incidents in chatRouter.
- Implemented chatService methods to ensure the chat report table is created and to handle incident data storage.
- Enhanced frontend components to allow users to report incidents in both multi and random chat dialogs.
- Updated internationalization files to include new strings for reporting functionality in multiple languages.
This commit is contained in:
Torsten Schulz (local)
2026-04-27 15:00:52 +02:00
parent 90e1c0496a
commit ff68fb72c4
12 changed files with 297 additions and 8 deletions

View File

@@ -784,9 +784,36 @@ class AdminService {
const types = await RegionType.findAll({
attributes: ['id', 'labelTr', 'parentId'],
order: [['labelTr', 'ASC']],
});
return types;
const byId = new Map(types.map((t) => [t.id, t]));
const depthCache = new Map();
const computeDepth = (typeId, visiting = new Set()) => {
if (depthCache.has(typeId)) return depthCache.get(typeId);
if (visiting.has(typeId)) {
// Cycle guard: treat as root-ish to avoid infinite recursion
depthCache.set(typeId, 0);
return 0;
}
visiting.add(typeId);
const t = byId.get(typeId);
const parentId = t?.parentId ?? null;
const depth = parentId ? (computeDepth(parentId, visiting) + 1) : 0;
visiting.delete(typeId);
depthCache.set(typeId, depth);
return depth;
};
// Sort by hierarchical level: roots (no parent) first, then children.
// Tie-breaker: labelTr for stable ordering.
const sorted = [...types].sort((a, b) => {
const da = computeDepth(a.id);
const db = computeDepth(b.id);
if (da !== db) return da - db;
return String(a.labelTr).localeCompare(String(b.labelTr));
});
return sorted;
}
async createFalukantRegion(userId, { name, regionTypeId, parentId } = {}) {

View File

@@ -18,6 +18,108 @@ class ChatService {
this.channel = null;
this.amqpAvailable = false;
this.initRabbitMq();
this._chatReportTableEnsured = false;
}
async _ensureChatReportTable() {
if (this._chatReportTableEnsured) return;
await Room.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.chat_report (
id bigserial PRIMARY KEY,
context varchar(40) NOT NULL,
reporter_hashed_id varchar(255) NULL,
reporter_random_id varchar(255) NULL,
reporter_username varchar(255) NULL,
offender_hashed_id varchar(255) NULL,
offender_random_id varchar(255) NULL,
offender_username varchar(255) NULL,
incident_at timestamptz NOT NULL,
chat_history jsonb NOT NULL,
metadata jsonb NULL,
created_at timestamptz NOT NULL DEFAULT NOW()
);
`);
await Room.sequelize.query(`
CREATE INDEX IF NOT EXISTS chat_report_created_idx
ON community.chat_report (created_at DESC);
`);
this._chatReportTableEnsured = true;
}
_normalizeChatHistory(history) {
if (!Array.isArray(history)) return [];
return history.slice(0, 1500).map((entry) => {
if (!entry || typeof entry !== 'object') {
return { text: String(entry || '').slice(0, 3000) };
}
return {
type: String(entry.type || '').slice(0, 40),
user: String(entry.user || '').slice(0, 255),
text: String(entry.text || '').slice(0, 3000),
timestamp: entry.timestamp || null,
};
});
}
async reportChatIncident(payload) {
await this._ensureChatReportTable();
const context = String(payload.context || '').trim();
if (!['random_chat', 'multi_chat', 'one_to_one'].includes(context)) {
throw new Error('invalid_context');
}
const incidentAt = payload.incidentAt ? new Date(payload.incidentAt) : new Date();
if (Number.isNaN(incidentAt.getTime())) {
throw new Error('invalid_incident_at');
}
const chatHistory = this._normalizeChatHistory(payload.chatHistory);
if (!chatHistory.length) {
throw new Error('empty_chat_history');
}
const metadata = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : null;
const rows = await Room.sequelize.query(
`
INSERT INTO community.chat_report (
context,
reporter_hashed_id,
reporter_random_id,
reporter_username,
offender_hashed_id,
offender_random_id,
offender_username,
incident_at,
chat_history,
metadata
) VALUES (
:context,
:reporterHashedId,
:reporterRandomId,
:reporterUsername,
:offenderHashedId,
:offenderRandomId,
:offenderUsername,
:incidentAt,
CAST(:chatHistory AS jsonb),
CAST(:metadata AS jsonb)
)
RETURNING id, context, incident_at AS "incidentAt", created_at AS "createdAt"
`,
{
replacements: {
context,
reporterHashedId: payload.reporterHashedId || null,
reporterRandomId: payload.reporterRandomId || null,
reporterUsername: payload.reporterUsername || null,
offenderHashedId: payload.offenderHashedId || null,
offenderRandomId: payload.offenderRandomId || null,
offenderUsername: payload.offenderUsername || null,
incidentAt: incidentAt.toISOString(),
chatHistory: JSON.stringify(chatHistory),
metadata: metadata ? JSON.stringify(metadata) : null,
},
type: Room.sequelize.QueryTypes.SELECT
}
);
return rows[0];
}
initRabbitMq() {