openvibers/openvibe.network Commits
Moderation audit log (WS-D task 1): records tools, games, wiki, blog, news, reviews, deals, coupons, trade and codes .moderation.action (common.moderation-action@1, Contracts 0.53.0), one row each with the service from the event's prefix (action, actor_subject, target type/id/owner, reason, details); the consumer's TOPICS (and so scripts/subscribe-events.js) cover them; the admin Moderation log filters by each. Pins: openvibe-contracts 0.49.0 → 0.53.0
Changes
@@@ -20,7 +20,7 @@"helmet": "^8.1.0","jsonwebtoken": "^9.0.0","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0","uuid": "^11.1.0",@@@ -1465,9 +1465,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.49.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",-- "integrity": "sha512-lapsLoBm3ZpndgiOMELdCBh2PJtuuyqj6SUhQsfwJkxnLdQfyhXfRFgiuLh+iQrg4kqtG9611191Mp8zAfk7fQ==",++ "version": "0.53.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0",++ "integrity": "sha512-U+/QMB9dYHF+1ZS7ZZf+aYDokI8nuZmMZh1qOvisT7iB+QQ4uxr03E3wHm1/qOr+IHlt+7BzFaOPVw6K6cOSxA==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -23,7 +23,7 @@"helmet": "^8.1.0","jsonwebtoken": "^9.0.0","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0","uuid": "^11.1.0",@@@ -390,10 +390,10 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa<!-- Moderation log (ADR-022): every staff action any service reports, from /api/v1/staff/moderation-audit --><div class="section" id="sec-modlog">-- <p class="muted" style="margin:0 0 10px">Staff actions reported by Chat, Live, Community, Tips and Billing. Each service enforces its own moderation; this is the record.</p>++ <p class="muted" style="margin:0 0 10px">Staff actions reported by Chat, Live, Community, Tips, Billing and the other services (Tools, Games, Wiki, Blog, News, Reviews, Deals, Coupons, Trade, Codes). Each service enforces its own moderation; this is the record.</p><form id="modlog-filters" style="display:flex;flex-wrap:wrap;gap:8px;align-items:end;margin-bottom:10px" onsubmit="event.preventDefault(); loadModLog(true)"><label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Service-- <select id="modlog-service" onchange="loadModLog(true)"><option value="">All</option><option value="chat">Chat</option><option value="live">Live</option><option value="community">Community</option><option value="tips">Tips</option><option value="billing">Billing</option></select></label>++ <select id="modlog-service" onchange="loadModLog(true)"><option value="">All</option><option value="chat">Chat</option><option value="live">Live</option><option value="community">Community</option><option value="tips">Tips</option><option value="billing">Billing</option><option value="blog">Blog</option><option value="codes">Codes</option><option value="coupons">Coupons</option><option value="deals">Deals</option><option value="games">Games</option><option value="news">News</option><option value="reviews">Reviews</option><option value="tools">Tools</option><option value="trade">Trade</option><option value="wiki">Wiki</option></select></label><label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Action<input id="modlog-action" placeholder="e.g. site_ban" maxlength="64" style="width:130px"></label><label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">By (subject)<input id="modlog-actor" placeholder="usr_…" maxlength="64" style="width:150px"></label><label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Target<input id="modlog-target" placeholder="subject or id" maxlength="200" style="width:150px"></label>@@@ -8,6 +8,10 @@* community.moderation.action staff actions on other people's content in Community* tips.interaction.moderated a paid message filtered, held, hidden or restored* billing.staff.action a staff money action in the Billing console++ * <service>.moderation.action common.moderation-action@1 (Contracts 0.53.0) from tools, games, wiki,++ * blog, news, reviews, deals, coupons, trade and codes: a staff or++ * moderator action on someone else's content there; the service is the++ * event's prefix** Rows are written by the Events consumer (server/notifications/events-consumer.js) inside its inbox* transaction, so a redelivery records nothing twice. GET /api/v1/staff/moderation-audit lists them for@@@ -19,7 +23,10 @@ const express = require('express');const { staff } = require('openvibe-contracts');const { staffClaims } = require('../auth/staff-claims');--const TOPICS = Object.freeze(['chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'tips.interaction.moderated', 'billing.staff.action']);++// Producers whose event is common.moderation-action@1 (action, target { type, id, owner_subject }, actor_subject, reason, details).++const COMMON_SERVICES = Object.freeze(['tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes']);++const TOPICS = Object.freeze(['chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',++ ...COMMON_SERVICES.map((svc) => `${svc}.moderation.action`)]);const str = (v, n) => (v == null || v === '' ? null : String(v).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, n));const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});const json = (v) => { const s = JSON.stringify(obj(v)); return s.length > 4000 ? JSON.stringify({ truncated: true }) : s; };@@@ -72,6 +79,20 @@ function rowOf(event) {target_type: 'interaction', target_id: str(p.interaction_id, 64), target_subject: str(c.id, 64), scope: str(`by:${p.by || 'unknown'}`, 64), reason: null,details: json({ moderation_state: p.moderation_state, cancelled_effects: p.cancelled_effects }) };}++ case 'tools.moderation.action':++ case 'games.moderation.action':++ case 'wiki.moderation.action':++ case 'blog.moderation.action':++ case 'news.moderation.action':++ case 'reviews.moderation.action':++ case 'deals.moderation.action':++ case 'coupons.moderation.action':++ case 'trade.moderation.action':++ case 'codes.moderation.action': {++ const t = obj(p.target);++ return { ...base, service: event.event_type.split('.')[0], action: str(p.action, 64) || 'unknown', actor_subject: str(p.actor_subject || actorId, 64),++ target_type: str(t.type, 40), target_id: str(t.id, 200), target_subject: str(t.owner_subject, 64), scope: null, reason: str(p.reason, 500), details: json(p.details) };++ }case 'billing.staff.action': {const t = obj(p.target);return { ...base, service: 'billing', action: str(p.action, 64) || 'unknown', actor_subject: str(actorId, 64),@@@ -58,7 +58,8 @@ const rows = (userId) => db.prepare('SELECT * FROM notifications WHERE user_id =(async () => {// The subscriptions this consumer is meant for, and nothing else.-- assert.deepStrictEqual(TOPICS, ['deals.watch.matched', 'trade.alert.triggered', 'live.stream.started', 'chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'tips.interaction.moderated', 'billing.staff.action']);++ assert.deepStrictEqual(TOPICS, ['deals.watch.matched', 'trade.alert.triggered', 'live.stream.started', 'chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',++ ...['tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes'].map((svc) => `${svc}.moderation.action`)]);assert.deepStrictEqual(topicsFrom([]), TOPICS, 'scripts/subscribe-events.js subscribes every topic');assert.deepStrictEqual(topicsFrom(['--topic', 'live.stream.started']), ['live.stream.started']);assert.throws(() => topicsFrom(['--topic', 'live.*']));@@@ -1,7 +1,7 @@'use strict';--// The moderation audit log (ADR-022): staff actions from Chat/Live, Community, Tips and Billing arrive as--// events, are recorded once (inside the consumer's inbox transaction), and staff with--// staff.moderation.logs can list them; nobody else can.++// The moderation audit log (ADR-022): staff actions from Chat/Live, Community, Tips, Billing and the ten++// services on common.moderation-action@1 arrive as events, are recorded once (inside the consumer's inbox++// transaction), and staff with staff.moderation.logs can list them; nobody else can.const assert = require('assert');const fs = require('fs');const os = require('os');@@@ -59,6 +59,29 @@ const ev = (type, payload, actor = { type: 'user', id: MOD }) => ({ event_id: idassert.strictEqual(audit.list({ limit: 2, before: page.next }).items.length, 2, 'paged by id');assert.strictEqual(rowOf(ev('chat.message.deleted', {})), null);++ // The ten services on common.moderation-action@1 (Contracts 0.53.0): subscribed, one row each, the service from the prefix.++ const contracts = require('openvibe-contracts');++ const { TOPICS } = require('../server/notifications/events-consumer');++ const COMMON = ['tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes'];++ for (const svc of COMMON) {++ const type = `${svc}.moderation.action`;++ assert.ok(TOPICS.includes(type), `${type} is one of the consumer's subscriptions`);++ const payload = { action: 'item.hidden', target: { type: `${svc}_item`, id: `${svc}-1`, owner_subject: TARGET }, actor_subject: MOD, reason: `${svc} reason`, details: { previous: 'visible' } };++ assert.ok(contracts.validate(`${type}@1`, payload).valid, `${type}: a valid payload`);++ assert.strictEqual(consumer.apply(ev(type, payload)).outcome, 'recorded', type);++ }++ for (const svc of COMMON) {++ const rows = audit.list({ service: svc }).items;++ assert.strictEqual(rows.length, 1, svc);++ const r = rows[0];++ assert.deepStrictEqual([r.action, r.actor_subject, r.target_type, r.target_id, r.target_subject, r.scope, r.reason, r.details],++ ['item.hidden', MOD, `${svc}_item`, `${svc}-1`, TARGET, null, `${svc} reason`, { previous: 'visible' }], svc);++ }++ assert.strictEqual(audit.list({ action: 'item.hidden' }).items.length, COMMON.length);++ // A service acting without a person names no actor; an unknown owner is no target subject.++ const bySvc = rowOf(ev('coupons.moderation.action', { action: 'merchant.disabled', target: { type: 'merchant', id: 'mer_1', owner_subject: null }, actor_subject: null, details: {} }, { type: 'service', id: 'coupons' }));++ assert.deepStrictEqual([bySvc.service, bySvc.action, bySvc.actor_subject, bySvc.target_subject, bySvc.reason], ['coupons', 'merchant.disabled', null, null, null]);++// Only staff with staff.moderation.logs read it.const app = express();app.use((req, _res, next) => { req.user = req.headers['x-role'] ? { id: 1, role: req.headers['x-role'] } : null; next(); });
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -1465,9 +1465,9 @@
@@ -1465,9 +1465,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.49.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
- "integrity": "sha512-lapsLoBm3ZpndgiOMELdCBh2PJtuuyqj6SUhQsfwJkxnLdQfyhXfRFgiuLh+iQrg4kqtG9611191Mp8zAfk7fQ==",
+ "version": "0.53.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0",
+ "integrity": "sha512-U+/QMB9dYHF+1ZS7ZZf+aYDokI8nuZmMZh1qOvisT7iB+QQ4uxr03E3wHm1/qOr+IHlt+7BzFaOPVw6K6cOSxA==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -390,10 +390,10 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa
@@ -390,10 +390,10 @@ body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMa
<!-- Moderation log (ADR-022): every staff action any service reports, from /api/v1/staff/moderation-audit -->
<!-- Moderation log (ADR-022): every staff action any service reports, from /api/v1/staff/moderation-audit -->
<div class="section" id="sec-modlog">
<div class="section" id="sec-modlog">
- <p class="muted" style="margin:0 0 10px">Staff actions reported by Chat, Live, Community, Tips and Billing. Each service enforces its own moderation; this is the record.</p>
+ <p class="muted" style="margin:0 0 10px">Staff actions reported by Chat, Live, Community, Tips, Billing and the other services (Tools, Games, Wiki, Blog, News, Reviews, Deals, Coupons, Trade, Codes). Each service enforces its own moderation; this is the record.</p>
<form id="modlog-filters" style="display:flex;flex-wrap:wrap;gap:8px;align-items:end;margin-bottom:10px" onsubmit="event.preventDefault(); loadModLog(true)">
<form id="modlog-filters" style="display:flex;flex-wrap:wrap;gap:8px;align-items:end;margin-bottom:10px" onsubmit="event.preventDefault(); loadModLog(true)">
<label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Service
<label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Service
- <select id="modlog-service" onchange="loadModLog(true)"><option value="">All</option><option value="chat">Chat</option><option value="live">Live</option><option value="community">Community</option><option value="tips">Tips</option><option value="billing">Billing</option></select></label>
+ <select id="modlog-service" onchange="loadModLog(true)"><option value="">All</option><option value="chat">Chat</option><option value="live">Live</option><option value="community">Community</option><option value="tips">Tips</option><option value="billing">Billing</option><option value="blog">Blog</option><option value="codes">Codes</option><option value="coupons">Coupons</option><option value="deals">Deals</option><option value="games">Games</option><option value="news">News</option><option value="reviews">Reviews</option><option value="tools">Tools</option><option value="trade">Trade</option><option value="wiki">Wiki</option></select></label>
<label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Action<input id="modlog-action" placeholder="e.g. site_ban" maxlength="64" style="width:130px"></label>
<label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Action<input id="modlog-action" placeholder="e.g. site_ban" maxlength="64" style="width:130px"></label>
<label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">By (subject)<input id="modlog-actor" placeholder="usr_…" maxlength="64" style="width:150px"></label>
<label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">By (subject)<input id="modlog-actor" placeholder="usr_…" maxlength="64" style="width:150px"></label>
<label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Target<input id="modlog-target" placeholder="subject or id" maxlength="200" style="width:150px"></label>
<label class="muted" style="font-size:12px;display:flex;flex-direction:column;gap:2px">Target<input id="modlog-target" placeholder="subject or id" maxlength="200" style="width:150px"></label>
@@ -8,6 +8,10 @@
@@ -8,6 +8,10 @@
* community.moderation.action staff actions on other people's content in Community
* community.moderation.action staff actions on other people's content in Community
* tips.interaction.moderated a paid message filtered, held, hidden or restored
* tips.interaction.moderated a paid message filtered, held, hidden or restored
* billing.staff.action a staff money action in the Billing console
* billing.staff.action a staff money action in the Billing console
+ * <service>.moderation.action common.moderation-action@1 (Contracts 0.53.0) from tools, games, wiki,
+ * blog, news, reviews, deals, coupons, trade and codes: a staff or
+ * moderator action on someone else's content there; the service is the
+ * event's prefix
*
*
* Rows are written by the Events consumer (server/notifications/events-consumer.js) inside its inbox
* Rows are written by the Events consumer (server/notifications/events-consumer.js) inside its inbox
* transaction, so a redelivery records nothing twice. GET /api/v1/staff/moderation-audit lists them for
* transaction, so a redelivery records nothing twice. GET /api/v1/staff/moderation-audit lists them for
@@ -19,7 +23,10 @@ const express = require('express');
@@ -19,7 +23,10 @@ const express = require('express');
const { staff } = require('openvibe-contracts');
const { staff } = require('openvibe-contracts');
const { staffClaims } = require('../auth/staff-claims');
const { staffClaims } = require('../auth/staff-claims');
-const TOPICS = Object.freeze(['chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'tips.interaction.moderated', 'billing.staff.action']);
+// Producers whose event is common.moderation-action@1 (action, target { type, id, owner_subject }, actor_subject, reason, details).
+const COMMON_SERVICES = Object.freeze(['tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes']);
+const TOPICS = Object.freeze(['chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',
+ ...COMMON_SERVICES.map((svc) => `${svc}.moderation.action`)]);
const str = (v, n) => (v == null || v === '' ? null : String(v).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, n));
const str = (v, n) => (v == null || v === '' ? null : String(v).replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, n));
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
const json = (v) => { const s = JSON.stringify(obj(v)); return s.length > 4000 ? JSON.stringify({ truncated: true }) : s; };
const json = (v) => { const s = JSON.stringify(obj(v)); return s.length > 4000 ? JSON.stringify({ truncated: true }) : s; };
@@ -72,6 +79,20 @@ function rowOf(event) {
@@ -72,6 +79,20 @@ function rowOf(event) {
target_type: 'interaction', target_id: str(p.interaction_id, 64), target_subject: str(c.id, 64), scope: str(`by:${p.by || 'unknown'}`, 64), reason: null,
target_type: 'interaction', target_id: str(p.interaction_id, 64), target_subject: str(c.id, 64), scope: str(`by:${p.by || 'unknown'}`, 64), reason: null,
details: json({ moderation_state: p.moderation_state, cancelled_effects: p.cancelled_effects }) };
details: json({ moderation_state: p.moderation_state, cancelled_effects: p.cancelled_effects }) };
}
}
+ case 'tools.moderation.action':
+ case 'games.moderation.action':
+ case 'wiki.moderation.action':
+ case 'blog.moderation.action':
+ case 'news.moderation.action':
+ case 'reviews.moderation.action':
+ case 'deals.moderation.action':
+ case 'coupons.moderation.action':
+ case 'trade.moderation.action':
+ case 'codes.moderation.action': {
+ const t = obj(p.target);
+ return { ...base, service: event.event_type.split('.')[0], action: str(p.action, 64) || 'unknown', actor_subject: str(p.actor_subject || actorId, 64),
+ target_type: str(t.type, 40), target_id: str(t.id, 200), target_subject: str(t.owner_subject, 64), scope: null, reason: str(p.reason, 500), details: json(p.details) };
+ }
case 'billing.staff.action': {
case 'billing.staff.action': {
const t = obj(p.target);
const t = obj(p.target);
return { ...base, service: 'billing', action: str(p.action, 64) || 'unknown', actor_subject: str(actorId, 64),
return { ...base, service: 'billing', action: str(p.action, 64) || 'unknown', actor_subject: str(actorId, 64),
@@ -58,7 +58,8 @@ const rows = (userId) => db.prepare('SELECT * FROM notifications WHERE user_id =
@@ -58,7 +58,8 @@ const rows = (userId) => db.prepare('SELECT * FROM notifications WHERE user_id =
(async () => {
(async () => {
// The subscriptions this consumer is meant for, and nothing else.
// The subscriptions this consumer is meant for, and nothing else.
- assert.deepStrictEqual(TOPICS, ['deals.watch.matched', 'trade.alert.triggered', 'live.stream.started', 'chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'tips.interaction.moderated', 'billing.staff.action']);
+ assert.deepStrictEqual(TOPICS, ['deals.watch.matched', 'trade.alert.triggered', 'live.stream.started', 'chat.moderation.action', 'live.moderation.action', 'community.moderation.action', 'tips.interaction.moderated', 'billing.staff.action',
+ ...['tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes'].map((svc) => `${svc}.moderation.action`)]);
assert.deepStrictEqual(topicsFrom([]), TOPICS, 'scripts/subscribe-events.js subscribes every topic');
assert.deepStrictEqual(topicsFrom([]), TOPICS, 'scripts/subscribe-events.js subscribes every topic');
assert.deepStrictEqual(topicsFrom(['--topic', 'live.stream.started']), ['live.stream.started']);
assert.deepStrictEqual(topicsFrom(['--topic', 'live.stream.started']), ['live.stream.started']);
assert.throws(() => topicsFrom(['--topic', 'live.*']));
assert.throws(() => topicsFrom(['--topic', 'live.*']));
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
-// The moderation audit log (ADR-022): staff actions from Chat/Live, Community, Tips and Billing arrive as
-// events, are recorded once (inside the consumer's inbox transaction), and staff with
-// staff.moderation.logs can list them; nobody else can.
+// The moderation audit log (ADR-022): staff actions from Chat/Live, Community, Tips, Billing and the ten
+// services on common.moderation-action@1 arrive as events, are recorded once (inside the consumer's inbox
+// transaction), and staff with staff.moderation.logs can list them; nobody else can.
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const os = require('os');
const os = require('os');
@@ -59,6 +59,29 @@ const ev = (type, payload, actor = { type: 'user', id: MOD }) => ({ event_id: id
@@ -59,6 +59,29 @@ const ev = (type, payload, actor = { type: 'user', id: MOD }) => ({ event_id: id
assert.strictEqual(audit.list({ limit: 2, before: page.next }).items.length, 2, 'paged by id');
assert.strictEqual(audit.list({ limit: 2, before: page.next }).items.length, 2, 'paged by id');
assert.strictEqual(rowOf(ev('chat.message.deleted', {})), null);
assert.strictEqual(rowOf(ev('chat.message.deleted', {})), null);
+ // The ten services on common.moderation-action@1 (Contracts 0.53.0): subscribed, one row each, the service from the prefix.
+ const contracts = require('openvibe-contracts');
+ const { TOPICS } = require('../server/notifications/events-consumer');
+ const COMMON = ['tools', 'games', 'wiki', 'blog', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes'];
+ for (const svc of COMMON) {
+ const type = `${svc}.moderation.action`;
+ assert.ok(TOPICS.includes(type), `${type} is one of the consumer's subscriptions`);
+ const payload = { action: 'item.hidden', target: { type: `${svc}_item`, id: `${svc}-1`, owner_subject: TARGET }, actor_subject: MOD, reason: `${svc} reason`, details: { previous: 'visible' } };
+ assert.ok(contracts.validate(`${type}@1`, payload).valid, `${type}: a valid payload`);
+ assert.strictEqual(consumer.apply(ev(type, payload)).outcome, 'recorded', type);
+ }
+ for (const svc of COMMON) {
+ const rows = audit.list({ service: svc }).items;
+ assert.strictEqual(rows.length, 1, svc);
+ const r = rows[0];
+ assert.deepStrictEqual([r.action, r.actor_subject, r.target_type, r.target_id, r.target_subject, r.scope, r.reason, r.details],
+ ['item.hidden', MOD, `${svc}_item`, `${svc}-1`, TARGET, null, `${svc} reason`, { previous: 'visible' }], svc);
+ }
+ assert.strictEqual(audit.list({ action: 'item.hidden' }).items.length, COMMON.length);
+ // A service acting without a person names no actor; an unknown owner is no target subject.
+ const bySvc = rowOf(ev('coupons.moderation.action', { action: 'merchant.disabled', target: { type: 'merchant', id: 'mer_1', owner_subject: null }, actor_subject: null, details: {} }, { type: 'service', id: 'coupons' }));
+ assert.deepStrictEqual([bySvc.service, bySvc.action, bySvc.actor_subject, bySvc.target_subject, bySvc.reason], ['coupons', 'merchant.disabled', null, null, null]);
+
// Only staff with staff.moderation.logs read it.
// Only staff with staff.moderation.logs read it.
const app = express();
const app = express();
app.use((req, _res, next) => { req.user = req.headers['x-role'] ? { id: 1, role: req.headers['x-role'] } : null; next(); });
app.use((req, _res, next) => { req.user = req.headers['x-role'] ? { id: 1, role: req.headers['x-role'] } : null; next(); });