mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Make each admin action self-explanatory and give content appeals a real effect instead of a dead-end "Dismiss". - submissionsSection.js: per-type action sets, each button paired with a one-line description. Applications show Approve (adds to whitelist) / Deny; appeals that name a real event show "Approve & unblock" (removes the video from the event block list) / "Keep blocked"; other types show "Mark handled". - submissionService.js: parse an appeal's `e` tag into targetEventId (trusted only when it's a well-formed 64-char hex id). - iframe-content-appeals-form.html: add a required "Blocked video link or event ID" field; regex-validate + normalize a share link / nevent / note / hex to the hex event id, tag it as `e`, and include it in the body. Block submission when no valid reference is provided. - CSS: stacked action + hint styling; iframe-form .form-hint. - tests: cover targetEventId parsing (valid, malformed, absent). Co-Authored-By: Claude Opus 4.8 <[email protected]>
140 lines
4.1 KiB
JavaScript
140 lines
4.1 KiB
JavaScript
// js/submissions/submissionService.js
|
|
//
|
|
// Structured bitvid submissions (#23) — pure core. A submission is a public,
|
|
// addressable kind-30083 event (SUBMISSION_KIND) published from an ephemeral key
|
|
// so a public form needs no login; the `applicant` tag carries the CLAIMED npub
|
|
// an admin vouches for. This module builds/parses those events with no relay or
|
|
// signer, so it's fully unit-testable. Fetch/publish/approve/deny live in the
|
|
// facade that wires this to the live client.
|
|
|
|
import { buildSubmissionEvent, SUBMISSION_KIND } from "../nostrEventSchemas.js";
|
|
|
|
export { SUBMISSION_KIND };
|
|
|
|
export const SUBMISSION_TYPES = Object.freeze({
|
|
APPLICATION: "application",
|
|
APPEAL: "appeal",
|
|
BUG: "bug",
|
|
FEATURE: "feature",
|
|
FEEDBACK: "feedback",
|
|
});
|
|
|
|
const KNOWN_TYPES = new Set(Object.values(SUBMISSION_TYPES));
|
|
|
|
// Human labels for the admin UI.
|
|
export const SUBMISSION_TYPE_LABELS = Object.freeze({
|
|
application: "Whitelist application",
|
|
appeal: "Content appeal",
|
|
bug: "Bug report",
|
|
feature: "Feature request",
|
|
feedback: "Feedback",
|
|
other: "Submission",
|
|
});
|
|
|
|
function firstTagValue(tags, name) {
|
|
const tag = tags.find(
|
|
(t) => Array.isArray(t) && t[0] === name && typeof t[1] === "string",
|
|
);
|
|
return tag ? tag[1] : "";
|
|
}
|
|
|
|
const HEX_EVENT_ID_REGEX = /^[0-9a-f]{64}$/i;
|
|
|
|
// The `e` tag on an appeal points at the blocked video's event id. We only trust
|
|
// a well-formed 64-char hex id — the admin approve path feeds it straight into
|
|
// the event block list.
|
|
function firstEventTarget(tags) {
|
|
for (const tag of tags) {
|
|
if (Array.isArray(tag) && tag[0] === "e" && typeof tag[1] === "string") {
|
|
const id = tag[1].trim().toLowerCase();
|
|
if (HEX_EVENT_ID_REGEX.test(id)) {
|
|
return id;
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Parse a kind-30083 event into a structured submission. Returns null for a
|
|
* non-submission event or one missing a `d` identifier.
|
|
* @param {Object} event
|
|
* @returns {null | {
|
|
* id: string, eventId: string, type: string, applicant: string,
|
|
* targetEventId: string, recipient: string, content: string,
|
|
* createdAt: number, pubkey: string,
|
|
* }}
|
|
*/
|
|
export function parseSubmissionEvent(event) {
|
|
if (!event || typeof event !== "object" || !Array.isArray(event.tags)) {
|
|
return null;
|
|
}
|
|
if (Number.isFinite(event.kind) && event.kind !== SUBMISSION_KIND) {
|
|
return null;
|
|
}
|
|
const tags = event.tags;
|
|
const id = firstTagValue(tags, "d").trim();
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
const rawType = firstTagValue(tags, "k").trim() || "application";
|
|
return {
|
|
id,
|
|
eventId: typeof event.id === "string" ? event.id : "",
|
|
type: KNOWN_TYPES.has(rawType) ? rawType : "other",
|
|
applicant: firstTagValue(tags, "applicant").trim(),
|
|
targetEventId: firstEventTarget(tags),
|
|
recipient: firstTagValue(tags, "p").trim(),
|
|
content: typeof event.content === "string" ? event.content : "",
|
|
createdAt: Number.isFinite(event.created_at) ? event.created_at : 0,
|
|
pubkey:
|
|
typeof event.pubkey === "string" ? event.pubkey.trim().toLowerCase() : "",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build an unsigned, signable submission event.
|
|
* @param {{
|
|
* pubkey: string, id: string, type?: string, applicantNpub?: string,
|
|
* recipientPubkey?: string, content?: string, created_at?: number,
|
|
* }} submission
|
|
* @returns {Object} unsigned kind-30083 event
|
|
*/
|
|
export function buildSubmission({
|
|
pubkey,
|
|
id,
|
|
type = "application",
|
|
applicantNpub = "",
|
|
recipientPubkey = "",
|
|
content = "",
|
|
created_at,
|
|
} = {}) {
|
|
return buildSubmissionEvent({
|
|
pubkey,
|
|
created_at: Number.isFinite(created_at)
|
|
? created_at
|
|
: Math.floor(Date.now() / 1000),
|
|
dTagValue: id,
|
|
submissionType: type,
|
|
applicantNpub,
|
|
recipientPubkey,
|
|
content,
|
|
});
|
|
}
|
|
|
|
/** Generate a stable-ish submission id for the `d` tag. */
|
|
export function generateSubmissionId() {
|
|
const time = Date.now().toString(36);
|
|
const rand = Math.random().toString(36).slice(2, 10);
|
|
return `sub-${time}-${rand}`;
|
|
}
|
|
|
|
export default {
|
|
SUBMISSION_KIND,
|
|
SUBMISSION_TYPES,
|
|
SUBMISSION_TYPE_LABELS,
|
|
parseSubmissionEvent,
|
|
buildSubmission,
|
|
generateSubmissionId,
|
|
};
|