Merge pull request #7 from PR0M3TH3AN/unstable

overall much better.
This commit is contained in:
thePR0M3TH3AN
2025-02-04 11:38:40 -05:00
committed by GitHub
19 changed files with 1922 additions and 1120 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"liveServer.settings.root": "./src"
}

41
site.webmanifest Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "bitvid - Decentralized Video Sharing",
"short_name": "bitvid",
"description": "seed. zap. subscribe.",
"icons": [
{
"src": "src/assets/png/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "src/assets/png/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "src/assets/png/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "src/assets/png/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "src/assets/png/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
}
],
"start_url": "src/index.html",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#0f172a",
"orientation": "portrait-primary",
"scope": "/",
"categories": ["video", "entertainment", "decentralized", "streaming"],
"related_applications": [],
"lang": "en"
}

View File

@@ -67,7 +67,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="https://beta.bitvid.network/components/iframe_forms/iframe-application-form.html"
src="./components/iframe_forms/iframe-application-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -56,7 +56,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="https://beta.bitvid.network/components/iframe_forms/iframe-bug-fix-form.html"
src="./components/iframe_forms/iframe-bug-fix-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -53,7 +53,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="https://beta.bitvid.network/components/iframe_forms/iframe-content-appeals-form.html"
src="./components/iframe_forms/iframe-content-appeals-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -53,7 +53,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="https://beta.bitvid.network/components/iframe_forms/iframe-request-form.html"
src="./components/iframe_forms/iframe-request-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -57,7 +57,7 @@
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="https://beta.bitvid.network/components/iframe_forms/iframe-feedback-form.html"
src="./components/iframe_forms/iframe-feedback-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<title>bitvid Whitelist Application Form</title>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="../../css/style.css" />
<style>
/* Override for form page to match modal field styling */
@@ -42,7 +42,7 @@
display: block;
margin-top: 1em;
font-weight: bold;
color: #E5E7EB; /* Tailwind's text-gray-200 */
color: #e5e7eb; /* Tailwind's text-gray-200 */
}
/* Input, textarea, select mimic modal field styles */
@@ -51,8 +51,8 @@
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1F2937; /* Tailwind's bg-gray-800 */
color: #F3F4F6; /* Tailwind's text-gray-100 */
background-color: #1f2937; /* Tailwind's bg-gray-800 */
color: #f3f4f6; /* Tailwind's text-gray-100 */
border: 1px solid #374151; /* Tailwind's border-gray-700 */
padding: 0.5em;
border-radius: 0.375rem; /* rounded-md */
@@ -62,9 +62,9 @@
input:focus,
textarea:focus,
select:focus {
border-color: #3B82F6; /* blue-500 */
border-color: #3b82f6; /* blue-500 */
outline: none;
box-shadow: 0 0 0 1px #3B82F6;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Style for checkboxes display inline with a label */
@@ -77,7 +77,7 @@
/* Button styled similarly to modal publish button */
button {
padding: 0.5em 1em;
background: #3B82F6; /* blue-500 */
background: #3b82f6; /* blue-500 */
color: #fff;
border: none;
border-radius: 0.375rem;
@@ -97,13 +97,13 @@
margin: 0.25em 0;
}
.error {
color: #F87171; /* a red tint */
color: #f87171; /* a red tint */
}
.success {
color: #3B82F6; /* blue-500 */
color: #3b82f6; /* blue-500 */
}
.warn {
color: #FACC15; /* a yellow tone */
color: #facc15; /* a yellow tone */
}
/* Custom Scrollbar styling for WebKit browsers */
@@ -115,13 +115,13 @@
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #3B82F6;
background-color: #3b82f6;
border-radius: 4px;
}
/* Custom Scrollbar styling for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #3B82F6 transparent;
scrollbar-color: #3b82f6 transparent;
}
</style>
<!-- Load nostrtools v2.10.4 -->
@@ -131,72 +131,163 @@
<div class="container">
<div class="form-container">
<p>
bitvid is currently in early access. If you would like to request access, please fill out this application. Applications will be reviewed manually, and approvals will be based on alignment with bitvids community values.
bitvid is currently in early access. If you would like to request
access, please fill out this application. Applications will be
reviewed manually, and approvals will be based on alignment with
bitvids community values.
</p>
<form id="wl-form">
<!-- 1. Applicant Information -->
<h2>1. Applicant Information</h2>
<label for="applicantNpub">Nostr Public Key (npub):</label>
<input type="text" id="applicantNpub" placeholder="Enter your npub" required />
<input
type="text"
id="applicantNpub"
placeholder="Enter your npub"
required
/>
<label for="contactMethod">Preferred Contact Method (if applicable):</label>
<input type="text" id="contactMethod" placeholder="Nostr DM, email, or other" />
<label for="contactMethod"
>Preferred Contact Method (if applicable):</label
>
<input
type="text"
id="contactMethod"
placeholder="Nostr DM, email, or other"
/>
<label for="username">Username or Alias (if applicable):</label>
<input type="text" id="username" placeholder="Enter your preferred name" />
<input
type="text"
id="username"
placeholder="Enter your preferred name"
/>
<!-- 2. Content Intent -->
<h2>2. Content Intent</h2>
<p>What type of content do you plan to upload? (Check all that apply)</p>
<p>
What type of content do you plan to upload? (Check all that apply)
</p>
<div class="checkbox-group">
<label><input type="checkbox" name="contentType" value="Educational" /> Educational</label>
<label><input type="checkbox" name="contentType" value="Entertainment" /> Entertainment</label>
<label><input type="checkbox" name="contentType" value="News & Journalism" /> News & Journalism</label>
<label><input type="checkbox" name="contentType" value="Creative Works" /> Creative Works</label>
<label><input type="checkbox" name="contentType" value="Discussions & Opinions" /> Discussions & Opinions</label>
<label><input type="checkbox" name="contentType" value="Other" /> Other</label>
<label
><input type="checkbox" name="contentType" value="Educational" />
Educational</label
>
<label
><input
type="checkbox"
name="contentType"
value="Entertainment"
/>
Entertainment</label
>
<label
><input
type="checkbox"
name="contentType"
value="News & Journalism"
/>
News & Journalism</label
>
<label
><input
type="checkbox"
name="contentType"
value="Creative Works"
/>
Creative Works</label
>
<label
><input
type="checkbox"
name="contentType"
value="Discussions & Opinions"
/>
Discussions & Opinions</label
>
<label
><input type="checkbox" name="contentType" value="Other" />
Other</label
>
</div>
<label for="otherContent">Other (please specify):</label>
<input type="text" id="otherContent" placeholder="Describe if Other was selected" />
<input
type="text"
id="otherContent"
placeholder="Describe if Other was selected"
/>
<label for="whyJoin">Why do you want to join bitvid?</label>
<textarea id="whyJoin" rows="3" placeholder="Explain your motivation"></textarea>
<textarea
id="whyJoin"
rows="3"
placeholder="Explain your motivation"
></textarea>
<label for="priorExperience">Have you created content on other platforms before?</label>
<label for="priorExperience"
>Have you created content on other platforms before?</label
>
<select id="priorExperience">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label for="experienceLinks">If yes, provide links or references to your previous work:</label>
<textarea id="experienceLinks" rows="2" placeholder="Paste links or describe your work"></textarea>
<label for="experienceLinks"
>If yes, provide links or references to your previous work:</label
>
<textarea
id="experienceLinks"
rows="2"
placeholder="Paste links or describe your work"
></textarea>
<!-- 3. Community Engagement -->
<h2>3. Community Engagement</h2>
<label for="familiarGuidelines">Are you familiar with bitvids Community Guidelines?</label>
<label for="familiarGuidelines"
>Are you familiar with bitvids Community Guidelines?</label
>
<select id="familiarGuidelines">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label for="agreeGuidelines">Do you agree to follow the guidelines and respect decentralized moderation?</label>
<label for="agreeGuidelines"
>Do you agree to follow the guidelines and respect decentralized
moderation?</label
>
<select id="agreeGuidelines">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label for="communityContribution">How do you plan to contribute positively to the bitvid community?</label>
<textarea id="communityContribution" rows="3" placeholder="Explain your approach"></textarea>
<label for="communityContribution"
>How do you plan to contribute positively to the bitvid
community?</label
>
<textarea
id="communityContribution"
rows="3"
placeholder="Explain your approach"
></textarea>
<!-- 4. Additional Information -->
<h2>4. Additional Information</h2>
<label for="specialSkills">Do you have any special skills or interests that could help improve bitvid?</label>
<textarea id="specialSkills" rows="3" placeholder="e.g., software development, moderation, design, advocacy"></textarea>
<label for="specialSkills"
>Do you have any special skills or interests that could help improve
bitvid?</label
>
<textarea
id="specialSkills"
rows="3"
placeholder="e.g., software development, moderation, design, advocacy"
></textarea>
<label for="testFeatures">Would you be interested in helping test new features?</label>
<label for="testFeatures"
>Would you be interested in helping test new features?</label
>
<select id="testFeatures">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
@@ -207,9 +298,10 @@
<h2>5. Declaration</h2>
<p>
By submitting this application, you confirm that:
<br />- The information provided is accurate.
<br />- You understand that approval is based on community alignment and available capacity.
<br />- You acknowledge that whitelist status may be revoked if guidelines are violated.
<br />- The information provided is accurate. <br />- You understand
that approval is based on community alignment and available
capacity. <br />- You acknowledge that whitelist status may be
revoked if guidelines are violated.
</p>
<label for="signature">Signature (Digital or Written):</label>
<input type="text" id="signature" placeholder="Your signature" />
@@ -242,53 +334,90 @@
log("NostrTools not loaded. Check console or ad-blockers.", "error");
return;
}
const { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip19, SimplePool, Relay } = window.NostrTools;
const {
generateSecretKey,
getPublicKey,
finalizeEvent,
nip04,
nip19,
SimplePool,
Relay,
} = window.NostrTools;
// Set the recipient's NPUB (your personal NPUB) here.
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const recipientNpub =
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const RELAYS = [
"wss://relay.snort.social",
"wss://relay.damus.io",
"wss://relay.primal.net"
"wss://relay.primal.net",
];
const pool = new SimplePool();
document.getElementById("wl-form").addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Retrieve applicant-provided input.
const applicantNpub = document.getElementById("applicantNpub").value.trim();
const contactMethod = document.getElementById("contactMethod").value.trim();
const username = document.getElementById("username").value.trim();
document
.getElementById("wl-form")
.addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Retrieve applicant-provided input.
const applicantNpub = document
.getElementById("applicantNpub")
.value.trim();
const contactMethod = document
.getElementById("contactMethod")
.value.trim();
const username = document.getElementById("username").value.trim();
// Section 2: Content Intent
// Get all checked content types
const contentTypeNodes = document.querySelectorAll('input[name="contentType"]:checked');
let contentTypes = [];
contentTypeNodes.forEach((node) => {
contentTypes.push(node.value);
});
const otherContent = document.getElementById("otherContent").value.trim();
const whyJoin = document.getElementById("whyJoin").value.trim();
const priorExperience = document.getElementById("priorExperience").value.trim();
const experienceLinks = document.getElementById("experienceLinks").value.trim();
// Section 2: Content Intent
// Get all checked content types
const contentTypeNodes = document.querySelectorAll(
'input[name="contentType"]:checked'
);
let contentTypes = [];
contentTypeNodes.forEach((node) => {
contentTypes.push(node.value);
});
const otherContent = document
.getElementById("otherContent")
.value.trim();
const whyJoin = document.getElementById("whyJoin").value.trim();
const priorExperience = document
.getElementById("priorExperience")
.value.trim();
const experienceLinks = document
.getElementById("experienceLinks")
.value.trim();
// Section 3: Community Engagement
const familiarGuidelines = document.getElementById("familiarGuidelines").value.trim();
const agreeGuidelines = document.getElementById("agreeGuidelines").value.trim();
const communityContribution = document.getElementById("communityContribution").value.trim();
// Section 3: Community Engagement
const familiarGuidelines = document
.getElementById("familiarGuidelines")
.value.trim();
const agreeGuidelines = document
.getElementById("agreeGuidelines")
.value.trim();
const communityContribution = document
.getElementById("communityContribution")
.value.trim();
// Section 4: Additional Information
const specialSkills = document.getElementById("specialSkills").value.trim();
const testFeatures = document.getElementById("testFeatures").value.trim();
// Section 4: Additional Information
const specialSkills = document
.getElementById("specialSkills")
.value.trim();
const testFeatures = document
.getElementById("testFeatures")
.value.trim();
// Section 5: Declaration
const signature = document.getElementById("signature").value.trim();
const declarationDate = document.getElementById("declarationDate").value.trim();
// Section 5: Declaration
const signature = document
.getElementById("signature")
.value.trim();
const declarationDate = document
.getElementById("declarationDate")
.value.trim();
// Construct the whitelist application content.
const applicationContent = `
// Construct the whitelist application content.
const applicationContent = `
# **bitvid Whitelist Application Form**
**1. Applicant Information**
@@ -301,13 +430,19 @@
- **Other (if applicable):** ${otherContent || "N/A"}
- **Why do you want to join bitvid?**
${whyJoin || "N/A"}
- **Have you created content on other platforms before?** ${priorExperience || "N/A"}
- **Have you created content on other platforms before?** ${
priorExperience || "N/A"
}
- **Links or references to your previous work:**
${experienceLinks || "N/A"}
**3. Community Engagement**
- **Familiar with bitvids Community Guidelines?** ${familiarGuidelines || "N/A"}
- **Agree to follow guidelines and respect moderation?** ${agreeGuidelines || "N/A"}
- **Familiar with bitvids Community Guidelines?** ${
familiarGuidelines || "N/A"
}
- **Agree to follow guidelines and respect moderation?** ${
agreeGuidelines || "N/A"
}
- **How do you plan to contribute to the community?**
${communityContribution || "N/A"}
@@ -332,71 +467,90 @@ By submitting this application, you confirm that:
For further questions, contact us through bitvids Nostr support channels.
`.trim();
log("[DEBUG] Constructed application content:\n" + applicationContent);
log(
"[DEBUG] Constructed application content:\n" +
applicationContent
);
// Decode the recipient NPUB to get the public key.
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
// Decode the recipient NPUB to get the public key.
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair.
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the application content.
log("Encrypting application content (nip04)...");
const ciphertext = await nip04.encrypt(
ephemeralPriv,
targetPubHex,
applicationContent
);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the Nostr event template.
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log(
"[DEBUG] Event template before finalizing: " +
JSON.stringify(eventTemplate)
);
// Finalize the event (computing the id and signature).
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish the event to all relays.
log("Publishing the application to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// For each relay, subscribe to verify the event appears.
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log(
"[" +
url +
"] => Found our application in storage! ID: " +
foundEvent.id.slice(0, 8) +
"...",
"success"
);
}
},
onEose() {
relay.close();
},
});
}
log(
"Done. If the logs show that at least one relay accepted the event and the application was found in storage, a moderator will review your application within 7-14 days."
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair.
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the application content.
log("Encrypting application content (nip04)...");
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, applicationContent);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the Nostr event template.
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
// Finalize the event (computing the id and signature).
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish the event to all relays.
log("Publishing the application to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// For each relay, subscribe to verify the event appears.
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log("[" + url + "] => Found our application in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
}
},
onEose() {
relay.close();
},
});
}
log("Done. If the logs show that at least one relay accepted the event and the application was found in storage, a moderator will review your application within 7-14 days.");
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
});
});
});
</script>
</body>

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<title>bitvid Bug Report Form</title>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="../../css/style.css" />
<style>
/* Override for form page to match modal field styling */
@@ -42,7 +42,7 @@
display: block;
margin-top: 1em;
font-weight: bold;
color: #E5E7EB; /* Tailwind's text-gray-200 */
color: #e5e7eb; /* Tailwind's text-gray-200 */
}
/* Input, textarea, and select mimic modal field styles */
@@ -51,8 +51,8 @@
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1F2937; /* Tailwind's bg-gray-800 */
color: #F3F4F6; /* Tailwind's text-gray-100 */
background-color: #1f2937; /* Tailwind's bg-gray-800 */
color: #f3f4f6; /* Tailwind's text-gray-100 */
border: 1px solid #374151; /* Tailwind's border-gray-700 */
padding: 0.5em;
border-radius: 0.375rem; /* rounded-md */
@@ -62,9 +62,9 @@
input:focus,
textarea:focus,
select:focus {
border-color: #3B82F6; /* blue-500 */
border-color: #3b82f6; /* blue-500 */
outline: none;
box-shadow: 0 0 0 1px #3B82F6;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Checkbox group styling */
@@ -77,7 +77,7 @@
/* Button styled similarly to modal publish button */
button {
padding: 0.5em 1em;
background: #3B82F6; /* blue-500 */
background: #3b82f6; /* blue-500 */
color: #fff;
border: none;
border-radius: 0.375rem;
@@ -97,13 +97,13 @@
margin: 0.25em 0;
}
.error {
color: #F87171; /* a red tint */
color: #f87171; /* a red tint */
}
.success {
color: #3B82F6; /* blue-500 */
color: #3b82f6; /* blue-500 */
}
.warn {
color: #FACC15; /* a yellow tone */
color: #facc15; /* a yellow tone */
}
/* Custom Scrollbar styling for WebKit browsers */
@@ -115,13 +115,13 @@
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #3B82F6;
background-color: #3b82f6;
border-radius: 4px;
}
/* Custom Scrollbar styling for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #3B82F6 transparent;
scrollbar-color: #3b82f6 transparent;
}
</style>
<!-- Load nostrtools v2.10.4 -->
@@ -131,44 +131,99 @@
<div class="container">
<div class="form-container">
<p>
If you have encountered a bug or technical issue on bitvid, please fill out this form to help us diagnose and resolve the problem. Providing detailed information will assist us in troubleshooting more efficiently.
If you have encountered a bug or technical issue on bitvid, please
fill out this form to help us diagnose and resolve the problem.
Providing detailed information will assist us in troubleshooting more
efficiently.
</p>
<form id="bug-form">
<!-- Section 1: User Information -->
<h2>1. User Information</h2>
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
<input type="text" id="userNpub" placeholder="Enter your npub (optional)" />
<input
type="text"
id="userNpub"
placeholder="Enter your npub (optional)"
/>
<p>Are you a (check all that apply):</p>
<div class="checkbox-group">
<label><input type="checkbox" name="userRole" value="Viewer" /> Viewer</label>
<label><input type="checkbox" name="userRole" value="Content Creator" /> Content Creator</label>
<label><input type="checkbox" name="userRole" value="Developer/Contributor" /> Developer / Contributor</label>
<label
><input type="checkbox" name="userRole" value="Viewer" />
Viewer</label
>
<label
><input type="checkbox" name="userRole" value="Content Creator" />
Content Creator</label
>
<label
><input
type="checkbox"
name="userRole"
value="Developer/Contributor"
/>
Developer / Contributor</label
>
</div>
<!-- Section 2: Bug Details -->
<h2>2. Bug Details</h2>
<label for="issueDescription">Describe the issue:</label>
<textarea id="issueDescription" rows="3" placeholder="Provide a clear and concise description of the problem"></textarea>
<textarea
id="issueDescription"
rows="3"
placeholder="Provide a clear and concise description of the problem"
></textarea>
<label for="stepsToReproduce">Steps to reproduce the bug:</label>
<textarea id="stepsToReproduce" rows="3" placeholder="List the actions taken before encountering the issue"></textarea>
<textarea
id="stepsToReproduce"
rows="3"
placeholder="List the actions taken before encountering the issue"
></textarea>
<label for="expectedBehavior">Expected behavior:</label>
<textarea id="expectedBehavior" rows="2" placeholder="What should have happened instead?"></textarea>
<textarea
id="expectedBehavior"
rows="2"
placeholder="What should have happened instead?"
></textarea>
<label for="actualBehavior">Actual behavior:</label>
<textarea id="actualBehavior" rows="2" placeholder="What actually happened?"></textarea>
<textarea
id="actualBehavior"
rows="2"
placeholder="What actually happened?"
></textarea>
<!-- Section 3: Device & Environment -->
<h2>3. Device &amp; Environment</h2>
<p>What device were you using? (check all that apply):</p>
<div class="checkbox-group">
<label><input type="checkbox" name="deviceUsed" value="Desktop" /> Desktop</label>
<label><input type="checkbox" name="deviceUsed" value="Mobile" /> Mobile</label>
<label><input type="checkbox" name="deviceUsed" value="Tablet" /> Tablet</label>
<label
><input type="checkbox" name="deviceUsed" value="Desktop" />
Desktop</label
>
<label
><input type="checkbox" name="deviceUsed" value="Mobile" />
Mobile</label
>
<label
><input type="checkbox" name="deviceUsed" value="Tablet" />
Tablet</label
>
</div>
<label for="operatingSystem">Operating System:</label>
<input type="text" id="operatingSystem" placeholder="e.g., Windows, macOS, Linux, iOS, Android" />
<input
type="text"
id="operatingSystem"
placeholder="e.g., Windows, macOS, Linux, iOS, Android"
/>
<label for="browserInfo">Browser (if applicable):</label>
<input type="text" id="browserInfo" placeholder="e.g., Chrome, Firefox, Safari" />
<label for="usingVPN">Are you using a VPN or privacy-focused browser settings?</label>
<input
type="text"
id="browserInfo"
placeholder="e.g., Chrome, Firefox, Safari"
/>
<label for="usingVPN"
>Are you using a VPN or privacy-focused browser settings?</label
>
<select id="usingVPN">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
@@ -177,12 +232,28 @@
<!-- Section 4: Screenshots or Logs -->
<h2>4. Screenshots or Logs (If Available)</h2>
<label for="screenshotInfo">Can you provide a screenshot or screen recording?</label>
<textarea id="screenshotInfo" rows="2" placeholder="Attach if possible (e.g., URL to screenshot)"></textarea>
<label for="errorMessages">Did the issue generate any error messages?</label>
<textarea id="errorMessages" rows="2" placeholder="Include the exact error messages, if any"></textarea>
<label for="screenshotInfo"
>Can you provide a screenshot or screen recording?</label
>
<textarea
id="screenshotInfo"
rows="2"
placeholder="Attach if possible (e.g., URL to screenshot)"
></textarea>
<label for="errorMessages"
>Did the issue generate any error messages?</label
>
<textarea
id="errorMessages"
rows="2"
placeholder="Include the exact error messages, if any"
></textarea>
<label for="consoleLogs">Any relevant console logs?</label>
<textarea id="consoleLogs" rows="2" placeholder="Paste any browser console logs if available"></textarea>
<textarea
id="consoleLogs"
rows="2"
placeholder="Paste any browser console logs if available"
></textarea>
<!-- Section 5: Additional Information -->
<h2>5. Additional Information</h2>
@@ -193,14 +264,23 @@
<option value="Occasionally">Occasionally</option>
<option value="Rarely">Rarely</option>
</select>
<label for="impactCore">Does this bug impact core functionality (e.g., video playback, uploads, etc.)?</label>
<label for="impactCore"
>Does this bug impact core functionality (e.g., video playback,
uploads, etc.)?</label
>
<select id="impactCore">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label for="additionalNotes">Any additional notes or suggestions?</label>
<textarea id="additionalNotes" rows="3" placeholder="Describe any other relevant details"></textarea>
<label for="additionalNotes"
>Any additional notes or suggestions?</label
>
<textarea
id="additionalNotes"
rows="3"
placeholder="Describe any other relevant details"
></textarea>
<button type="submit">Submit Bug Report</button>
</form>
@@ -234,58 +314,89 @@
nip04,
nip19,
SimplePool,
Relay
Relay,
} = window.NostrTools;
// Set the recipient's NPUB (your personal NPUB)
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const recipientNpub =
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const RELAYS = [
"wss://relay.snort.social",
"wss://relay.damus.io",
"wss://relay.primal.net"
"wss://relay.primal.net",
];
const pool = new SimplePool();
document.getElementById("bug-form").addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
const roleNodes = document.querySelectorAll('input[name="userRole"]:checked');
let userRoles = [];
roleNodes.forEach(node => {
userRoles.push(node.value);
});
// Section 2: Bug Details
const issueDescription = document.getElementById("issueDescription").value.trim();
const stepsToReproduce = document.getElementById("stepsToReproduce").value.trim();
const expectedBehavior = document.getElementById("expectedBehavior").value.trim();
const actualBehavior = document.getElementById("actualBehavior").value.trim();
document
.getElementById("bug-form")
.addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
const roleNodes = document.querySelectorAll(
'input[name="userRole"]:checked'
);
let userRoles = [];
roleNodes.forEach((node) => {
userRoles.push(node.value);
});
// Section 3: Device & Environment
const deviceNodes = document.querySelectorAll('input[name="deviceUsed"]:checked');
let devicesUsed = [];
deviceNodes.forEach(node => {
devicesUsed.push(node.value);
});
const operatingSystem = document.getElementById("operatingSystem").value.trim();
const browserInfo = document.getElementById("browserInfo").value.trim();
const usingVPN = document.getElementById("usingVPN").value.trim();
// Section 2: Bug Details
const issueDescription = document
.getElementById("issueDescription")
.value.trim();
const stepsToReproduce = document
.getElementById("stepsToReproduce")
.value.trim();
const expectedBehavior = document
.getElementById("expectedBehavior")
.value.trim();
const actualBehavior = document
.getElementById("actualBehavior")
.value.trim();
// Section 4: Screenshots or Logs
const screenshotInfo = document.getElementById("screenshotInfo").value.trim();
const errorMessages = document.getElementById("errorMessages").value.trim();
const consoleLogs = document.getElementById("consoleLogs").value.trim();
// Section 3: Device & Environment
const deviceNodes = document.querySelectorAll(
'input[name="deviceUsed"]:checked'
);
let devicesUsed = [];
deviceNodes.forEach((node) => {
devicesUsed.push(node.value);
});
const operatingSystem = document
.getElementById("operatingSystem")
.value.trim();
const browserInfo = document
.getElementById("browserInfo")
.value.trim();
const usingVPN = document.getElementById("usingVPN").value.trim();
// Section 5: Additional Information
const bugFrequency = document.getElementById("bugFrequency").value.trim();
const impactCore = document.getElementById("impactCore").value.trim();
const additionalNotes = document.getElementById("additionalNotes").value.trim();
// Section 4: Screenshots or Logs
const screenshotInfo = document
.getElementById("screenshotInfo")
.value.trim();
const errorMessages = document
.getElementById("errorMessages")
.value.trim();
const consoleLogs = document
.getElementById("consoleLogs")
.value.trim();
// Construct the bug report content as Markdown.
const bugReportContent = `
// Section 5: Additional Information
const bugFrequency = document
.getElementById("bugFrequency")
.value.trim();
const impactCore = document
.getElementById("impactCore")
.value.trim();
const additionalNotes = document
.getElementById("additionalNotes")
.value.trim();
// Construct the bug report content as Markdown.
const bugReportContent = `
# **bitvid Bug Report Form**
If you have encountered a bug or technical issue on bitvid, please fill out this form to help us diagnose and resolve the problem. Providing detailed information will assist us in troubleshooting more efficiently.
@@ -328,71 +439,89 @@ Our team reviews bug reports regularly. While we aim to fix critical issues as s
For urgent issues, contact us through bitvids Nostr support channels.
`.trim();
log("[DEBUG] Constructed bug report content:\n" + bugReportContent);
log(
"[DEBUG] Constructed bug report content:\n" + bugReportContent
);
// Decode the recipient NPUB to get the public key.
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
// Decode the recipient NPUB to get the public key.
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair.
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the bug report content.
log("Encrypting bug report content (nip04)...");
const ciphertext = await nip04.encrypt(
ephemeralPriv,
targetPubHex,
bugReportContent
);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the event template.
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log(
"[DEBUG] Event template before finalizing: " +
JSON.stringify(eventTemplate)
);
// Finalize the event.
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish the event to all relays.
log("Publishing the bug report to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// Subscribe to each relay.
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log(
"[" +
url +
"] => Found our bug report in storage! ID: " +
foundEvent.id.slice(0, 8) +
"...",
"success"
);
}
},
onEose() {
relay.close();
},
});
}
log(
"Done. If the logs show that at least one relay accepted the event and the bug report was found in storage, our team will review your report within 7-14 days."
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair.
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the bug report content.
log("Encrypting bug report content (nip04)...");
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, bugReportContent);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the event template.
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
// Finalize the event.
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish the event to all relays.
log("Publishing the bug report to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// Subscribe to each relay.
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log("[" + url + "] => Found our bug report in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
}
},
onEose() {
relay.close();
},
});
}
log("Done. If the logs show that at least one relay accepted the event and the bug report was found in storage, our team will review your report within 7-14 days.");
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
});
});
});
</script>
</body>

View File

@@ -4,10 +4,10 @@
<meta charset="utf-8" />
<title>bitvid Content Appeals Form</title>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="../../css/style.css" />
<style>
/* Override for form page to match modal field styling */
/* Remove width constraints from body so our container can be full width */
body {
background-color: transparent;
@@ -15,7 +15,7 @@
font-family: system-ui, -apple-system, sans-serif;
margin: 20px;
}
/* Override the .container to use the full available width */
.container {
width: 100%;
@@ -23,7 +23,7 @@
margin: 0;
padding: 0;
}
/* Card-like container for the form, similar to modal-content */
.form-container {
background-color: #111827; /* Tailwind's bg-gray-900 */
@@ -31,52 +31,52 @@
border-radius: 0.5rem;
box-shadow: var(--shadow-md);
}
h1,
h2 {
color: #fff;
}
/* Labels in a light gray */
label {
display: block;
margin-top: 1em;
font-weight: bold;
color: #E5E7EB; /* Tailwind's text-gray-200 */
color: #e5e7eb; /* Tailwind's text-gray-200 */
}
/* Input, textarea, and select mimic modal field styles */
input,
textarea,
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1F2937; /* Tailwind's bg-gray-800 */
color: #F3F4F6; /* Tailwind's text-gray-100 */
background-color: #1f2937; /* Tailwind's bg-gray-800 */
color: #f3f4f6; /* Tailwind's text-gray-100 */
border: 1px solid #374151; /* Tailwind's border-gray-700 */
padding: 0.5em;
border-radius: 0.375rem; /* rounded-md */
box-sizing: border-box;
}
input:focus,
textarea:focus,
select:focus {
border-color: #3B82F6; /* blue-500 */
border-color: #3b82f6; /* blue-500 */
outline: none;
box-shadow: 0 0 0 1px #3B82F6;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Button styled similarly to modal publish button */
button {
padding: 0.5em 1em;
background: #3B82F6; /* blue-500 */
background: #3b82f6; /* blue-500 */
color: #fff;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
/* Status log area */
#status {
margin-top: 1em;
@@ -90,15 +90,15 @@
margin: 0.25em 0;
}
.error {
color: #F87171; /* a red tint */
color: #f87171; /* a red tint */
}
.success {
color: #3B82F6; /* blue-500 */
color: #3b82f6; /* blue-500 */
}
.warn {
color: #FACC15; /* a yellow tone */
color: #facc15; /* a yellow tone */
}
/* Custom Scrollbar styling for WebKit browsers */
::-webkit-scrollbar {
width: 8px;
@@ -108,13 +108,13 @@
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #3B82F6;
background-color: #3b82f6;
border-radius: 4px;
}
/* Custom Scrollbar styling for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #3B82F6 transparent;
scrollbar-color: #3b82f6 transparent;
}
</style>
<!-- Load nostrtools v2.10.4 -->
@@ -124,70 +124,115 @@
<div class="container">
<div class="form-container">
<p>
If you believe your content was unfairly blocked or restricted on bitvid,
please complete this form. Appeals will be reviewed manually, and
decisions will be communicated back to you.
If you believe your content was unfairly blocked or restricted on
bitvid, please complete this form. Appeals will be reviewed manually,
and decisions will be communicated back to you.
</p>
<form id="dm-form">
<!-- NPUB input removed as the recipient is now set by the code -->
<h2>1. User Information</h2>
<label for="contactMethod">Contact Method (if applicable):</label>
<input type="text" id="contactMethod" placeholder="Nostr DM, email, or other" />
<input
type="text"
id="contactMethod"
placeholder="Nostr DM, email, or other"
/>
<h2>2. Content Details</h2>
<label for="videoTitle">Title of the Video:</label>
<input type="text" id="videoTitle" placeholder="Enter the exact title" />
<input
type="text"
id="videoTitle"
placeholder="Enter the exact title"
/>
<label for="magnetLink">Magnet Link:</label>
<input type="text" id="magnetLink" placeholder="Enter the magnet link" />
<input
type="text"
id="magnetLink"
placeholder="Enter the magnet link"
/>
<label for="submissionDate">Date of Content Submission:</label>
<input type="date" id="submissionDate" />
<h2>3. Reason for Appeal</h2>
<label for="reasonBlocked">Why do you believe your content was unfairly blocked?</label>
<textarea id="reasonBlocked" rows="3" placeholder="Explain in detail"></textarea>
<label for="fitsGuidelines">Does your content fit within bitvid's Community Guidelines?</label>
<label for="reasonBlocked"
>Why do you believe your content was unfairly blocked?</label
>
<textarea
id="reasonBlocked"
rows="3"
placeholder="Explain in detail"
></textarea>
<label for="fitsGuidelines"
>Does your content fit within bitvid's Community Guidelines?</label
>
<select id="fitsGuidelines">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label for="guidelinesCited">If yes, which guideline(s) support your appeal?</label>
<textarea id="guidelinesCited" rows="2" placeholder="Cite the specific guidelines"></textarea>
<label for="editedContent">Was this content edited after being blocked?</label>
<label for="guidelinesCited"
>If yes, which guideline(s) support your appeal?</label
>
<textarea
id="guidelinesCited"
rows="2"
placeholder="Cite the specific guidelines"
></textarea>
<label for="editedContent"
>Was this content edited after being blocked?</label
>
<select id="editedContent">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label for="changesMade">If yes, what changes were made?</label>
<textarea id="changesMade" rows="2" placeholder="Describe the modifications"></textarea>
<textarea
id="changesMade"
rows="2"
placeholder="Describe the modifications"
></textarea>
<h2>4. Additional Context</h2>
<label for="misunderstanding">Was there any misunderstanding or misclassification?</label>
<textarea id="misunderstanding" rows="2" placeholder="Provide context"></textarea>
<label for="externalReferences">Are there external references that validate your appeal?</label>
<textarea id="externalReferences" rows="2" placeholder="Links, citations, or additional info"></textarea>
<label for="misunderstanding"
>Was there any misunderstanding or misclassification?</label
>
<textarea
id="misunderstanding"
rows="2"
placeholder="Provide context"
></textarea>
<label for="externalReferences"
>Are there external references that validate your appeal?</label
>
<textarea
id="externalReferences"
rows="2"
placeholder="Links, citations, or additional info"
></textarea>
<h2>5. Declaration</h2>
<p>
By submitting this appeal, you confirm that:
<br />- You are the original creator or authorized representative of the content.
<br />- Your appeal is submitted in good faith and aligns with bitvids policies.
<br />- You understand that final decisions are at the discretion of bitvids moderation process.
<br />- You are the original creator or authorized representative of
the content. <br />- Your appeal is submitted in good faith and
aligns with bitvids policies. <br />- You understand that final
decisions are at the discretion of bitvids moderation process.
</p>
<label for="signature">Signature (Digital or Written):</label>
<input type="text" id="signature" placeholder="Your signature" />
<label for="declarationDate">Date:</label>
<input type="date" id="declarationDate" />
<button type="submit">Submit Appeal</button>
</form>
<div id="status"></div>
@@ -213,41 +258,78 @@
log("NostrTools not loaded. Check console or ad-blockers.", "error");
return;
}
const { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip19, SimplePool, Relay } = window.NostrTools;
const {
generateSecretKey,
getPublicKey,
finalizeEvent,
nip04,
nip19,
SimplePool,
Relay,
} = window.NostrTools;
// Set the recipient's NPUB (your personal NPUB) here.
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const recipientNpub =
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const RELAYS = [
"wss://relay.snort.social",
"wss://relay.damus.io",
"wss://relay.primal.net"
"wss://relay.primal.net",
];
const pool = new SimplePool();
document.getElementById("dm-form").addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Note: The customer's NPUB input is removed.
// Use the recipientNpub constant to get the public key.
const contactMethod = document.getElementById("contactMethod").value.trim();
const videoTitle = document.getElementById("videoTitle").value.trim();
const magnetLink = document.getElementById("magnetLink").value.trim();
const submissionDate = document.getElementById("submissionDate").value.trim();
const reasonBlocked = document.getElementById("reasonBlocked").value.trim();
const fitsGuidelines = document.getElementById("fitsGuidelines").value.trim();
const guidelinesCited = document.getElementById("guidelinesCited").value.trim();
const editedContent = document.getElementById("editedContent").value.trim();
const changesMade = document.getElementById("changesMade").value.trim();
const misunderstanding = document.getElementById("misunderstanding").value.trim();
const externalReferences = document.getElementById("externalReferences").value.trim();
const signature = document.getElementById("signature").value.trim();
const declarationDate = document.getElementById("declarationDate").value.trim();
// Construct the appeal content.
const appealContent = `
document
.getElementById("dm-form")
.addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Note: The customer's NPUB input is removed.
// Use the recipientNpub constant to get the public key.
const contactMethod = document
.getElementById("contactMethod")
.value.trim();
const videoTitle = document
.getElementById("videoTitle")
.value.trim();
const magnetLink = document
.getElementById("magnetLink")
.value.trim();
const submissionDate = document
.getElementById("submissionDate")
.value.trim();
const reasonBlocked = document
.getElementById("reasonBlocked")
.value.trim();
const fitsGuidelines = document
.getElementById("fitsGuidelines")
.value.trim();
const guidelinesCited = document
.getElementById("guidelinesCited")
.value.trim();
const editedContent = document
.getElementById("editedContent")
.value.trim();
const changesMade = document
.getElementById("changesMade")
.value.trim();
const misunderstanding = document
.getElementById("misunderstanding")
.value.trim();
const externalReferences = document
.getElementById("externalReferences")
.value.trim();
const signature = document
.getElementById("signature")
.value.trim();
const declarationDate = document
.getElementById("declarationDate")
.value.trim();
// Construct the appeal content.
const appealContent = `
# **bitvid Content Appeals Form**
**1. User Information**
@@ -291,72 +373,88 @@ By submitting this appeal, you confirm that:
For further questions, reach out through bitvids Nostr support channels.
`.trim();
log("[DEBUG] Constructed appeal content:\n" + appealContent);
// Decode the recipient NPUB to get the public key.
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
log("[DEBUG] Constructed appeal content:\n" + appealContent);
// Decode the recipient NPUB to get the public key.
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair.
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the appeal content.
log("Encrypting appeal content (nip04)...");
const ciphertext = await nip04.encrypt(
ephemeralPriv,
targetPubHex,
appealContent
);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the event template.
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log(
"[DEBUG] Event template before finalizing: " +
JSON.stringify(eventTemplate)
);
// Finalize the event.
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish the event to all relays.
log("Publishing the appeal to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// Subscribe to each relay.
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log(
"[" +
url +
"] => Found our appeal in storage! ID: " +
foundEvent.id.slice(0, 8) +
"...",
"success"
);
}
},
onEose() {
relay.close();
},
});
}
log(
"Done. If the logs show that at least one relay accepted the event and the appeal was found in storage, a moderator will review your appeal within 7-14 days."
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair.
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the appeal content.
log("Encrypting appeal content (nip04)...");
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, appealContent);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the event template.
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
// Finalize the event.
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish the event to all relays.
log("Publishing the appeal to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// Subscribe to each relay.
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log("[" + url + "] => Found our appeal in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
}
},
onEose() {
relay.close();
},
});
}
log("Done. If the logs show that at least one relay accepted the event and the appeal was found in storage, a moderator will review your appeal within 7-14 days.");
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
});
});
});
</script>
</body>

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<title>bitvid General Feedback Form</title>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="../../css/style.css" />
<style>
/* Override for form page to match modal field styling */
@@ -42,7 +42,7 @@
display: block;
margin-top: 1em;
font-weight: bold;
color: #E5E7EB; /* Tailwind's text-gray-200 */
color: #e5e7eb; /* Tailwind's text-gray-200 */
}
/* Input, textarea, and select styling */
@@ -51,8 +51,8 @@
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1F2937; /* Tailwind's bg-gray-800 */
color: #F3F4F6; /* Tailwind's text-gray-100 */
background-color: #1f2937; /* Tailwind's bg-gray-800 */
color: #f3f4f6; /* Tailwind's text-gray-100 */
border: 1px solid #374151; /* Tailwind's border-gray-700 */
padding: 0.5em;
border-radius: 0.375rem; /* rounded-md */
@@ -62,9 +62,9 @@
input:focus,
textarea:focus,
select:focus {
border-color: #3B82F6; /* blue-500 */
border-color: #3b82f6; /* blue-500 */
outline: none;
box-shadow: 0 0 0 1px #3B82F6;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Checkbox group styling */
@@ -84,7 +84,7 @@
/* Button styling */
button {
padding: 0.5em 1em;
background: #3B82F6; /* blue-500 */
background: #3b82f6; /* blue-500 */
color: #fff;
border: none;
border-radius: 0.375rem;
@@ -104,13 +104,13 @@
margin: 0.25em 0;
}
.error {
color: #F87171;
color: #f87171;
}
.success {
color: #3B82F6;
color: #3b82f6;
}
.warn {
color: #FACC15;
color: #facc15;
}
/* Custom Scrollbar styling for WebKit browsers */
@@ -122,13 +122,13 @@
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #3B82F6;
background-color: #3b82f6;
border-radius: 4px;
}
/* Custom Scrollbar styling for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #3B82F6 transparent;
scrollbar-color: #3b82f6 transparent;
}
</style>
<!-- Load nostrtools v2.10.4 -->
@@ -138,48 +138,112 @@
<div class="container">
<div class="form-container">
<p>
Your feedback helps us improve bitvid! Whether its a suggestion, a concern, or general thoughts on the platform, wed love to hear from you.
Your feedback helps us improve bitvid! Whether its a suggestion, a
concern, or general thoughts on the platform, wed love to hear from
you.
</p>
<form id="feedback-form">
<!-- Section 1: User Information -->
<h2>1. User Information</h2>
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
<input type="text" id="userNpub" placeholder="Enter your npub (optional)" />
<input
type="text"
id="userNpub"
placeholder="Enter your npub (optional)"
/>
<p>Are you a (check all that apply):</p>
<div class="checkbox-group">
<label><input type="checkbox" name="userRole" value="Viewer" /> Viewer</label>
<label><input type="checkbox" name="userRole" value="Content Creator" /> Content Creator</label>
<label><input type="checkbox" name="userRole" value="Developer/Contributor" /> Developer / Contributor</label>
<label
><input type="checkbox" name="userRole" value="Viewer" />
Viewer</label
>
<label
><input type="checkbox" name="userRole" value="Content Creator" />
Content Creator</label
>
<label
><input
type="checkbox"
name="userRole"
value="Developer/Contributor"
/>
Developer / Contributor</label
>
</div>
<!-- Section 2: General Feedback -->
<h2>2. General Feedback</h2>
<label>How would you rate your experience on bitvid so far?</label>
<div class="radio-group">
<label><input type="radio" name="experienceRating" value="Excellent" /> Excellent</label>
<label><input type="radio" name="experienceRating" value="Good" /> Good</label>
<label><input type="radio" name="experienceRating" value="Average" /> Average</label>
<label><input type="radio" name="experienceRating" value="Needs Improvement" /> Needs Improvement</label>
<label
><input type="radio" name="experienceRating" value="Excellent" />
Excellent</label
>
<label
><input type="radio" name="experienceRating" value="Good" />
Good</label
>
<label
><input type="radio" name="experienceRating" value="Average" />
Average</label
>
<label
><input
type="radio"
name="experienceRating"
value="Needs Improvement"
/>
Needs Improvement</label
>
</div>
<label for="likeMost">What do you like most about bitvid?</label>
<textarea id="likeMost" rows="3" placeholder="Describe the features, usability, or content you enjoy"></textarea>
<textarea
id="likeMost"
rows="3"
placeholder="Describe the features, usability, or content you enjoy"
></textarea>
<label for="improvements">What would you like to see improved?</label>
<textarea id="improvements" rows="3" placeholder="Provide specific areas for improvement"></textarea>
<label for="confusingFeatures">Are there any features or tools you find confusing or difficult to use?</label>
<textarea id="confusingFeatures" rows="3" placeholder="Explain any challenges youve encountered"></textarea>
<textarea
id="improvements"
rows="3"
placeholder="Provide specific areas for improvement"
></textarea>
<label for="confusingFeatures"
>Are there any features or tools you find confusing or difficult to
use?</label
>
<textarea
id="confusingFeatures"
rows="3"
placeholder="Explain any challenges youve encountered"
></textarea>
<!-- Section 3: Additional Comments -->
<h2>3. Additional Comments</h2>
<label for="otherSuggestions">Do you have any other suggestions or thoughts about bitvid?</label>
<textarea id="otherSuggestions" rows="3" placeholder="Share any other feedback"></textarea>
<label for="followUp">Would you like to be contacted for follow-up discussions?</label>
<label for="otherSuggestions"
>Do you have any other suggestions or thoughts about bitvid?</label
>
<textarea
id="otherSuggestions"
rows="3"
placeholder="Share any other feedback"
></textarea>
<label for="followUp"
>Would you like to be contacted for follow-up discussions?</label
>
<select id="followUp">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<label for="preferredContact">Preferred contact method (if applicable):</label>
<input type="text" id="preferredContact" placeholder="Nostr DM, email, or other" />
<label for="preferredContact"
>Preferred contact method (if applicable):</label
>
<input
type="text"
id="preferredContact"
placeholder="Nostr DM, email, or other"
/>
<button type="submit">Submit General Feedback</button>
</form>
@@ -206,51 +270,78 @@
log("NostrTools not loaded. Check console or ad-blockers.", "error");
return;
}
const { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip19, SimplePool, Relay } = window.NostrTools;
const {
generateSecretKey,
getPublicKey,
finalizeEvent,
nip04,
nip19,
SimplePool,
Relay,
} = window.NostrTools;
// Set the recipient's NPUB (your personal NPUB)
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const recipientNpub =
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const RELAYS = [
"wss://relay.snort.social",
"wss://relay.damus.io",
"wss://relay.primal.net"
"wss://relay.primal.net",
];
const pool = new SimplePool();
document.getElementById("feedback-form").addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
document
.getElementById("feedback-form")
.addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
const roleNodes = document.querySelectorAll('input[name="userRole"]:checked');
let userRoles = [];
roleNodes.forEach(node => {
userRoles.push(node.value);
});
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
const roleNodes = document.querySelectorAll(
'input[name="userRole"]:checked'
);
let userRoles = [];
roleNodes.forEach((node) => {
userRoles.push(node.value);
});
// Section 2: General Feedback
const experienceRadio = document.querySelector('input[name="experienceRating"]:checked');
const experienceRating = experienceRadio ? experienceRadio.value : "N/A";
const likeMost = document.getElementById("likeMost").value.trim();
const improvements = document.getElementById("improvements").value.trim();
const confusingFeatures = document.getElementById("confusingFeatures").value.trim();
// Section 2: General Feedback
const experienceRadio = document.querySelector(
'input[name="experienceRating"]:checked'
);
const experienceRating = experienceRadio
? experienceRadio.value
: "N/A";
const likeMost = document.getElementById("likeMost").value.trim();
const improvements = document
.getElementById("improvements")
.value.trim();
const confusingFeatures = document
.getElementById("confusingFeatures")
.value.trim();
// Section 3: Additional Comments
const otherSuggestions = document.getElementById("otherSuggestions").value.trim();
const followUp = document.getElementById("followUp").value.trim();
const preferredContact = document.getElementById("preferredContact").value.trim();
// Section 3: Additional Comments
const otherSuggestions = document
.getElementById("otherSuggestions")
.value.trim();
const followUp = document.getElementById("followUp").value.trim();
const preferredContact = document
.getElementById("preferredContact")
.value.trim();
// Construct the Markdown feedback content
const feedbackContent = `
// Construct the Markdown feedback content
const feedbackContent = `
# **bitvid General Feedback Form**
Your feedback helps us improve bitvid! Whether its a suggestion, a concern, or general thoughts on the platform, wed love to hear from you.
## **1. User Information**
- **Nostr Public Key (npub) (optional):** ${userNpub || "N/A"}
- **Are you a (check all that apply):** ${userRoles.length > 0 ? userRoles.join(", ") : "N/A"}
- **Are you a (check all that apply):** ${
userRoles.length > 0 ? userRoles.join(", ") : "N/A"
}
## **2. General Feedback**
- **How would you rate your experience on bitvid so far?** ${experienceRating}
@@ -264,7 +355,9 @@ Your feedback helps us improve bitvid! Whether its a suggestion, a concern, o
## **3. Additional Comments**
- **Do you have any other suggestions or thoughts about bitvid?**
${otherSuggestions || "N/A"}
- **Would you like to be contacted for follow-up discussions?** ${followUp || "N/A"}
- **Would you like to be contacted for follow-up discussions?** ${
followUp || "N/A"
}
- **Preferred contact method (if applicable):** ${preferredContact || "N/A"}
---
@@ -274,71 +367,87 @@ We review all feedback regularly to improve bitvid. While not all suggestions ma
For additional discussions, reach out via bitvids Nostr support channels.
`.trim();
log("[DEBUG] Constructed feedback content:\n" + feedbackContent);
log("[DEBUG] Constructed feedback content:\n" + feedbackContent);
// Decode the recipient NPUB to get the public key.
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
// Decode the recipient NPUB to get the public key.
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair.
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the feedback content.
log("Encrypting feedback content (nip04)...");
const ciphertext = await nip04.encrypt(
ephemeralPriv,
targetPubHex,
feedbackContent
);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the event template.
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log(
"[DEBUG] Event template before finalizing: " +
JSON.stringify(eventTemplate)
);
// Finalize the event.
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish the event to all relays.
log("Publishing the feedback to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// Subscribe to each relay to verify storage.
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log(
"[" +
url +
"] => Found our feedback in storage! ID: " +
foundEvent.id.slice(0, 8) +
"...",
"success"
);
}
},
onEose() {
relay.close();
},
});
}
log(
"Done. If the logs show that at least one relay accepted the event and the feedback was stored, it will be reviewed accordingly."
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair.
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the feedback content.
log("Encrypting feedback content (nip04)...");
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, feedbackContent);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the event template.
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
// Finalize the event.
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish the event to all relays.
log("Publishing the feedback to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// Subscribe to each relay to verify storage.
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log("[" + url + "] => Found our feedback in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
}
},
onEose() {
relay.close();
},
});
}
log("Done. If the logs show that at least one relay accepted the event and the feedback was stored, it will be reviewed accordingly.");
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
});
});
});
</script>
</body>

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<title>bitvid Feature Request Form</title>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="../../css/style.css" />
<style>
/* Override for form page to match modal field styling */
@@ -42,7 +42,7 @@
display: block;
margin-top: 1em;
font-weight: bold;
color: #E5E7EB;
color: #e5e7eb;
}
/* Input, textarea, and select styling */
@@ -51,8 +51,8 @@
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1F2937;
color: #F3F4F6;
background-color: #1f2937;
color: #f3f4f6;
border: 1px solid #374151;
padding: 0.5em;
border-radius: 0.375rem;
@@ -62,9 +62,9 @@
input:focus,
textarea:focus,
select:focus {
border-color: #3B82F6;
border-color: #3b82f6;
outline: none;
box-shadow: 0 0 0 1px #3B82F6;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Checkbox group styling */
@@ -77,7 +77,7 @@
/* Button styling */
button {
padding: 0.5em 1em;
background: #3B82F6;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 0.375rem;
@@ -97,13 +97,13 @@
margin: 0.25em 0;
}
.error {
color: #F87171;
color: #f87171;
}
.success {
color: #3B82F6;
color: #3b82f6;
}
.warn {
color: #FACC15;
color: #facc15;
}
/* Custom Scrollbar styling */
@@ -115,12 +115,12 @@
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #3B82F6;
background-color: #3b82f6;
border-radius: 4px;
}
* {
scrollbar-width: thin;
scrollbar-color: #3B82F6 transparent;
scrollbar-color: #3b82f6 transparent;
}
</style>
<!-- Load nostrtools v2.10.4 -->
@@ -130,38 +130,87 @@
<div class="container">
<div class="form-container">
<p>
Have an idea for improving bitvid? Wed love to hear it! Please use this form to request new features or enhancements. Your feedback helps shape the future of bitvid.
Have an idea for improving bitvid? Wed love to hear it! Please use
this form to request new features or enhancements. Your feedback helps
shape the future of bitvid.
</p>
<form id="feature-form">
<!-- Section 1: User Information -->
<h2>1. User Information</h2>
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
<input type="text" id="userNpub" placeholder="Enter your npub (optional)" />
<input
type="text"
id="userNpub"
placeholder="Enter your npub (optional)"
/>
<p>Are you a (check all that apply):</p>
<div class="checkbox-group">
<label><input type="checkbox" name="userRole" value="Viewer" /> Viewer</label>
<label><input type="checkbox" name="userRole" value="Content Creator" /> Content Creator</label>
<label><input type="checkbox" name="userRole" value="Developer/Contributor" /> Developer / Contributor</label>
<label
><input type="checkbox" name="userRole" value="Viewer" />
Viewer</label
>
<label
><input type="checkbox" name="userRole" value="Content Creator" />
Content Creator</label
>
<label
><input
type="checkbox"
name="userRole"
value="Developer/Contributor"
/>
Developer / Contributor</label
>
</div>
<!-- Section 2: Feature Request Details -->
<h2>2. Feature Request Details</h2>
<label for="featureName">Feature Name:</label>
<input type="text" id="featureName" placeholder="Short, descriptive title" required />
<input
type="text"
id="featureName"
placeholder="Short, descriptive title"
required
/>
<label for="featureDescription">Describe the feature:</label>
<textarea id="featureDescription" rows="3" placeholder="Explain in detail what the feature does and how it works"></textarea>
<textarea
id="featureDescription"
rows="3"
placeholder="Explain in detail what the feature does and how it works"
></textarea>
<label for="featureImportance">Why is this feature important?</label>
<textarea id="featureImportance" rows="2" placeholder="Describe how this will improve the platform and benefit users"></textarea>
<textarea
id="featureImportance"
rows="2"
placeholder="Describe how this will improve the platform and benefit users"
></textarea>
<label for="beneficiary">Who would benefit from this feature?</label>
<input type="text" id="beneficiary" placeholder="e.g., Content Creators, Viewers, Both" />
<input
type="text"
id="beneficiary"
placeholder="e.g., Content Creators, Viewers, Both"
/>
<!-- Section 3: Additional Information -->
<h2>3. Additional Information</h2>
<label for="existingPlatforms">Are there existing platforms that have this feature?</label>
<textarea id="existingPlatforms" rows="2" placeholder="Provide examples if applicable"></textarea>
<label for="existingPlatforms"
>Are there existing platforms that have this feature?</label
>
<textarea
id="existingPlatforms"
rows="2"
placeholder="Provide examples if applicable"
></textarea>
<label for="mockups">Do you have any mockups or examples?</label>
<textarea id="mockups" rows="2" placeholder="Links, screenshots, or diagrams"></textarea>
<label for="willingToTest">Would you be willing to help test this feature if implemented?</label>
<textarea
id="mockups"
rows="2"
placeholder="Links, screenshots, or diagrams"
></textarea>
<label for="willingToTest"
>Would you be willing to help test this feature if
implemented?</label
>
<select id="willingToTest">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
@@ -172,12 +221,27 @@
<h2>4. Priority &amp; Impact</h2>
<p>How urgent is this feature?</p>
<div class="checkbox-group">
<label><input type="checkbox" name="featurePriority" value="High" /> High (Essential for platform success)</label>
<label><input type="checkbox" name="featurePriority" value="Medium" /> Medium (Would improve experience significantly)</label>
<label><input type="checkbox" name="featurePriority" value="Low" /> Low (Nice to have, but not urgent)</label>
<label
><input type="checkbox" name="featurePriority" value="High" />
High (Essential for platform success)</label
>
<label
><input type="checkbox" name="featurePriority" value="Medium" />
Medium (Would improve experience significantly)</label
>
<label
><input type="checkbox" name="featurePriority" value="Low" /> Low
(Nice to have, but not urgent)</label
>
</div>
<label for="techChallenges">Does this feature require technical expertise to implement?</label>
<textarea id="techChallenges" rows="2" placeholder="Describe any dependencies or challenges"></textarea>
<label for="techChallenges"
>Does this feature require technical expertise to implement?</label
>
<textarea
id="techChallenges"
rows="2"
placeholder="Describe any dependencies or challenges"
></textarea>
<button type="submit">Submit Feature Request</button>
</form>
@@ -204,50 +268,79 @@
log("NostrTools not loaded. Check console or ad-blockers.", "error");
return;
}
const { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip19, SimplePool, Relay } = window.NostrTools;
const {
generateSecretKey,
getPublicKey,
finalizeEvent,
nip04,
nip19,
SimplePool,
Relay,
} = window.NostrTools;
// Set the recipient's NPUB (your personal NPUB)
const recipientNpub = "npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const recipientNpub =
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
const RELAYS = [
"wss://relay.snort.social",
"wss://relay.damus.io",
"wss://relay.primal.net"
"wss://relay.primal.net",
];
const pool = new SimplePool();
document.getElementById("feature-form").addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
const roleNodes = document.querySelectorAll('input[name="userRole"]:checked');
let userRoles = [];
roleNodes.forEach(node => {
userRoles.push(node.value);
});
// Section 2: Feature Request Details
const featureName = document.getElementById("featureName").value.trim();
const featureDescription = document.getElementById("featureDescription").value.trim();
const featureImportance = document.getElementById("featureImportance").value.trim();
const beneficiary = document.getElementById("beneficiary").value.trim();
document
.getElementById("feature-form")
.addEventListener("submit", async (ev) => {
ev.preventDefault();
clear();
try {
// Section 1: User Information
const userNpub = document.getElementById("userNpub").value.trim();
const roleNodes = document.querySelectorAll(
'input[name="userRole"]:checked'
);
let userRoles = [];
roleNodes.forEach((node) => {
userRoles.push(node.value);
});
// Section 3: Additional Information
const existingPlatforms = document.getElementById("existingPlatforms").value.trim();
const mockups = document.getElementById("mockups").value.trim();
const willingToTest = document.getElementById("willingToTest").value.trim();
// Section 2: Feature Request Details
const featureName = document
.getElementById("featureName")
.value.trim();
const featureDescription = document
.getElementById("featureDescription")
.value.trim();
const featureImportance = document
.getElementById("featureImportance")
.value.trim();
const beneficiary = document
.getElementById("beneficiary")
.value.trim();
// Section 4: Priority & Impact
const priorityNodes = document.querySelectorAll('input[name="featurePriority"]:checked');
let featurePriorities = [];
priorityNodes.forEach(node => {
featurePriorities.push(node.value);
});
const techChallenges = document.getElementById("techChallenges").value.trim();
// Section 3: Additional Information
const existingPlatforms = document
.getElementById("existingPlatforms")
.value.trim();
const mockups = document.getElementById("mockups").value.trim();
const willingToTest = document
.getElementById("willingToTest")
.value.trim();
// Construct Markdown feature request
const featureRequestContent = `
// Section 4: Priority & Impact
const priorityNodes = document.querySelectorAll(
'input[name="featurePriority"]:checked'
);
let featurePriorities = [];
priorityNodes.forEach((node) => {
featurePriorities.push(node.value);
});
const techChallenges = document
.getElementById("techChallenges")
.value.trim();
// Construct Markdown feature request
const featureRequestContent = `
# **bitvid Feature Request Form**
Have an idea for improving bitvid? Wed love to hear it! Please use this form to request new features or enhancements. Your feedback helps shape the future of bitvid.
@@ -270,7 +363,9 @@ Have an idea for improving bitvid? Wed love to hear it! Please use this form
${existingPlatforms || "N/A"}
- **Do you have any mockups or examples?**
${mockups || "N/A"}
- **Would you be willing to help test this feature if implemented?** ${willingToTest || "N/A"}
- **Would you be willing to help test this feature if implemented?** ${
willingToTest || "N/A"
}
## **4. Priority & Impact**
- **How urgent is this feature?**
@@ -286,71 +381,90 @@ All feature requests are reviewed, but not all may be implemented. We prioritize
For further discussions, reach out through bitvids Nostr support channels.
`.trim();
log("[DEBUG] Constructed feature request content:\n" + featureRequestContent);
log(
"[DEBUG] Constructed feature request content:\n" +
featureRequestContent
);
// Decode the recipient NPUB
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
// Decode the recipient NPUB
log("Decoding recipient npub...");
const decoded = nip19.decode(recipientNpub);
log("[DEBUG] Decoded npub: " + JSON.stringify(decoded));
if (decoded.type !== "npub") {
throw new Error("Decoded type is not npub.");
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the feature request content
log("Encrypting feature request content (nip04)...");
const ciphertext = await nip04.encrypt(
ephemeralPriv,
targetPubHex,
featureRequestContent
);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the event template
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log(
"[DEBUG] Event template before finalizing: " +
JSON.stringify(eventTemplate)
);
// Finalize the event
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish to relays
log("Publishing the feature request to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// Subscribe to verify event storage
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log(
"[" +
url +
"] => Found our feature request in storage! ID: " +
foundEvent.id.slice(0, 8) +
"...",
"success"
);
}
},
onEose() {
relay.close();
},
});
}
log(
"Done. If the logs show that at least one relay accepted the event and the feature request was stored, it will be reviewed accordingly."
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
const targetPubHex = decoded.data;
log("Recipient pubkey: " + targetPubHex.slice(0, 16) + "...");
// Generate an ephemeral key pair
log("Generating ephemeral key...");
const ephemeralPriv = generateSecretKey();
const ephemeralPubHex = getPublicKey(ephemeralPriv);
log("Ephemeral pubkey: " + ephemeralPubHex.slice(0, 16) + "...");
// Encrypt the feature request content
log("Encrypting feature request content (nip04)...");
const ciphertext = await nip04.encrypt(ephemeralPriv, targetPubHex, featureRequestContent);
log("[DEBUG] Ciphertext: " + ciphertext);
log("Encryption done.");
// Build the event template
const now = Math.floor(Date.now() / 1000);
const eventTemplate = {
kind: 4,
created_at: now,
tags: [["p", targetPubHex]],
content: ciphertext,
};
log("[DEBUG] Event template before finalizing: " + JSON.stringify(eventTemplate));
// Finalize the event
const event = finalizeEvent(eventTemplate, ephemeralPriv);
log("[DEBUG] Final event: " + JSON.stringify(event));
// Publish to relays
log("Publishing the feature request to relays...");
await Promise.any(pool.publish(RELAYS, event));
log("At least one relay accepted the event.", "success");
// Subscribe to verify event storage
for (const url of RELAYS) {
log("Connecting to " + url + " for subscription...");
const relay = await Relay.connect(url);
relay.subscribe([{ authors: [ephemeralPubHex], kinds: [4] }], {
onEvent(foundEvent) {
if (foundEvent.id === event.id) {
log("[" + url + "] => Found our feature request in storage! ID: " + foundEvent.id.slice(0, 8) + "...", "success");
}
},
onEose() {
relay.close();
},
});
}
log("Done. If the logs show that at least one relay accepted the event and the feature request was stored, it will be reviewed accordingly.");
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
}
});
});
});
</script>
</body>

View File

@@ -475,32 +475,66 @@
if (appModal) {
appModal.classList.add("hidden");
}
// ADDED: If user has not seen disclaimer yet, show it after application modal is closed
if (!localStorage.getItem("hasSeenDisclaimer")) {
const disclaimerModal =
document.getElementById("disclaimerModal");
if (disclaimerModal) {
disclaimerModal.classList.remove("hidden");
}
}
});
}
//
// 5) ?modal=appeals => open content appeals form
// ?modal=application => open application form
//
const urlParams = new URLSearchParams(window.location.search);
const modalParam = urlParams.get("modal");
if (modalParam === "appeals") {
const appealsModal = document.getElementById("contentAppealsModal");
if (appealsModal) {
appealsModal.classList.remove("hidden");
}
}
//
// 5.1) ?modal=application => open application form
//
else if (modalParam === "application") {
// ADDED: After user closes appeals, show disclaimer if needed
const closeAppealsBtn = document.getElementById(
"closeContentAppealsModal"
);
if (closeAppealsBtn) {
closeAppealsBtn.addEventListener("click", () => {
appealsModal.classList.add("hidden");
if (!localStorage.getItem("hasSeenDisclaimer")) {
const disclaimerModal =
document.getElementById("disclaimerModal");
if (disclaimerModal) {
disclaimerModal.classList.remove("hidden");
}
}
});
}
} else if (modalParam === "application") {
// Show application form, but DO NOT show disclaimer until user closes
const appModal = document.getElementById("nostrFormModal");
if (appModal) {
appModal.classList.remove("hidden");
}
// Note: The close event above (closeNostrFormBtn) handles the disclaimer after closing.
} else {
// If there's no special param in the URL, we can consider showing the disclaimer right away
const hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer");
if (!hasSeenDisclaimer) {
const disclaimerModal = document.getElementById("disclaimerModal");
if (disclaimerModal) {
disclaimerModal.classList.remove("hidden");
}
}
}
//
// 6) Close content appeals modal
// 6) Close content appeals modal (needed if user navigates w/o param, then opens appeals)
//
const closeAppealsBtn = document.getElementById(
"closeContentAppealsModal"
@@ -515,23 +549,23 @@
}
//
// 7) Show disclaimer modal on page load, hide on "I Understand"
// 7) Disclaimer 'I Understand' Button
//
const disclaimerModal = document.getElementById("disclaimerModal");
const acceptDisclaimerBtn = document.getElementById("acceptDisclaimer");
if (disclaimerModal) {
// Show immediately
disclaimerModal.classList.remove("hidden");
if (acceptDisclaimerBtn) {
acceptDisclaimerBtn.addEventListener("click", () => {
if (acceptDisclaimerBtn) {
acceptDisclaimerBtn.addEventListener("click", () => {
// Hide disclaimer
const disclaimerModal = document.getElementById("disclaimerModal");
if (disclaimerModal) {
disclaimerModal.classList.add("hidden");
});
}
}
// Store the fact that user has seen it
localStorage.setItem("hasSeenDisclaimer", "true");
});
}
//
// 8) Query param checks for the three new forms
//
// https://bitvid.network?modal=feedback => open generalFeedbackModal
// https://bitvid.network?modal=feature => open featureRequestModal
// https://bitvid.network?modal=bug => open bugFixModal
@@ -599,6 +633,10 @@
<!-- Other Scripts -->
<script src="js/libs/nostr.bundle.js"></script>
<script type="module">
import { nip19, SimplePool } from "https://esm.sh/nostr-tools@1.8.3";
window.NostrTools = { nip19, SimplePool };
</script>
<script type="module" src="js/config.js"></script>
<script type="module" src="js/lists.js"></script>
<script type="module" src="js/accessControl.js"></script>

View File

@@ -5,6 +5,7 @@ import { nostrClient } from "./nostr.js";
import { torrentClient } from "./webtorrent.js";
import { isDevMode } from "./config.js";
import { disclaimerModal } from "./disclaimer.js";
import { initialBlacklist, initialEventBlacklist } from "./lists.js";
/**
* Simple "decryption" placeholder for private videos.
@@ -82,6 +83,27 @@ class bitvidApp {
// NEW: reference to the login modal's close button
this.closeLoginModalBtn =
document.getElementById("closeLoginModal") || null;
// Build a set of blacklisted event IDs (hex) from nevent strings, skipping empties
this.blacklistedEventIds = new Set();
for (const neventStr of initialEventBlacklist) {
// Skip any empty or obviously invalid strings
if (!neventStr || neventStr.trim().length < 8) {
continue;
}
try {
const decoded = window.NostrTools.nip19.decode(neventStr);
if (decoded.type === "nevent" && decoded.data.id) {
this.blacklistedEventIds.add(decoded.data.id);
}
} catch (err) {
console.error(
"[bitvidApp] Invalid nevent in blacklist:",
neventStr,
err
);
}
}
}
forceRefreshAllProfiles() {
@@ -99,6 +121,13 @@ class bitvidApp {
async init() {
try {
// Force update of any registered service workers to ensure latest code is used.
if ("serviceWorker" in navigator) {
navigator.serviceWorker.getRegistrations().then((registrations) => {
registrations.forEach((registration) => registration.update());
});
}
// 1. Initialize the video modal (components/video-modal.html)
await this.initModal();
this.updateModalElements();
@@ -693,51 +722,58 @@ class bitvidApp {
}
/**
* Subscribe to new videos & render them.
* Subscribe to videos (older + new) and render them as they come in.
*/
// js/app.js
async loadVideos() {
console.log("Starting loadVideos...");
// 1) If there's an existing subscription, unsubscribe it
if (this.videoSubscription) {
this.videoSubscription.unsub();
this.videoSubscription = null;
}
// We do NOT decode initialEventBlacklist here.
// That happens once in the constructor, creating this.blacklistedEventIds.
// 2) Show "Loading..." message
if (this.videoList) {
this.videoList.innerHTML = `
<p class="text-center text-gray-500">
Loading videos...
</p>`;
}
try {
// 3) Force a bulk fetch
await nostrClient.fetchVideos();
// 4) Instead of reusing the entire fetched array,
// use getActiveVideos() for the final display:
const newestActive = nostrClient.getActiveVideos();
this.renderVideoList(newestActive);
// 5) Subscribe for updates
this.videoSubscription = nostrClient.subscribeVideos((video) => {
// Whenever we get a new or updated event, re-render the newest set:
const activeAll = nostrClient.getActiveVideos();
this.renderVideoList(activeAll);
});
} catch (err) {
console.error("Could not load videos:", err);
this.showError("Could not load videos from relays.");
if (!this.videoSubscription) {
if (this.videoList) {
this.videoList.innerHTML = `
<p class="text-center text-gray-500">
No videos available at this time.
Loading videos as they arrive...
</p>`;
}
// Create a single subscription
this.videoSubscription = nostrClient.subscribeVideos(() => {
const updatedAll = nostrClient.getActiveVideos();
// Filter out blacklisted authors & blacklisted event IDs
const filteredVideos = updatedAll.filter((video) => {
// 1) If the event ID is in our blacklisted set, skip
if (this.blacklistedEventIds.has(video.id)) {
return false;
}
// 2) Check author (if youre also blacklisting authors by npub)
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (initialBlacklist.includes(authorNpub)) {
return false;
}
return true;
});
this.renderVideoList(filteredVideos);
});
} else {
// Already subscribed: just show what's cached
const allCached = nostrClient.getActiveVideos();
const filteredCached = allCached.filter((video) => {
if (this.blacklistedEventIds.has(video.id)) {
return false;
}
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
return !initialBlacklist.includes(authorNpub);
});
this.renderVideoList(filteredCached);
}
}
@@ -764,7 +800,7 @@ class bitvidApp {
if (!videos || videos.length === 0) {
this.videoList.innerHTML = `
<p class="text-center text-gray-500">
<p class="flex justify-center items-center h-full w-full text-center text-gray-500">
No public videos available yet. Be the first to upload one!
</p>`;
return;
@@ -1017,26 +1053,27 @@ class bitvidApp {
return;
}
// Look up the video in our subscription map
// 1) Check local 'videosMap' or 'nostrClient.getActiveVideos()'
let matchedVideo = Array.from(this.videosMap.values()).find(
(v) => v.magnet === decodedMagnet
);
// If not found in the map, do a fallback fetch
if (!matchedVideo) {
const allVideos = await nostrClient.fetchVideos();
matchedVideo = allVideos.find((v) => v.magnet === decodedMagnet);
// Instead of forcing a full `fetchVideos()`,
// try looking in the activeVideos from local cache:
const activeVideos = nostrClient.getActiveVideos();
matchedVideo = activeVideos.find((v) => v.magnet === decodedMagnet);
}
// If still not found, you can do a single event-based approach or just show an error:
if (!matchedVideo) {
this.showError("No matching video found.");
this.showError("No matching video found in local cache.");
return;
}
// Update our tracking
// Update tracking
this.currentMagnetUri = decodedMagnet;
// Hand off to the method that already sets modal fields and streams
// Delegate to the main method
await this.playVideoByEventId(matchedVideo.id);
} catch (error) {
console.error("Error in playVideo:", error);
@@ -1172,6 +1209,10 @@ class bitvidApp {
// 8) Refresh local UI
await this.loadVideos();
// 8.1) Purge the outdated cache
this.videosMap.clear();
this.showSuccess("Video updated successfully!");
// 9) Also refresh all profile caches so any new name/pic changes are reflected
@@ -1309,16 +1350,20 @@ class bitvidApp {
* Helper to open a video by event ID (like ?v=...).
*/
async playVideoByEventId(eventId) {
// First, check if this event is blacklisted
if (this.blacklistedEventIds.has(eventId)) {
this.showError("This content has been removed or is not allowed.");
return;
}
try {
// 1) Check local subscription map
let video = this.videosMap.get(eventId);
// 2) If not in local map, attempt fallback fetch from getOldEventById
if (!video) {
video = await this.getOldEventById(eventId);
}
// 3) If still no luck, show error and return
// 3) If still not found, show error and return
if (!video) {
this.showError("Video not found.");
return;
@@ -1368,8 +1413,9 @@ class bitvidApp {
// 8) Render video details in modal
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (this.videoTitle)
if (this.videoTitle) {
this.videoTitle.textContent = video.title || "Untitled";
}
if (this.videoDescription) {
this.videoDescription.textContent =
video.description || "No description available.";
@@ -1391,14 +1437,18 @@ class bitvidApp {
this.creatorAvatar.alt = creatorProfile.name;
}
// 9) Stream torrent
this.log("Starting video stream with:", video.magnet);
// 9) Clean up any existing torrent instance before starting a new stream
await torrentClient.cleanup();
// 10) Append a cache-busting parameter to the magnet URI
const cacheBustedMagnet = video.magnet + "&ts=" + Date.now();
this.log("Starting video stream with:", cacheBustedMagnet);
const realTorrent = await torrentClient.streamVideo(
video.magnet,
cacheBustedMagnet,
this.modalVideo
);
// 10) Start intervals to update stats
// 11) Start intervals to update stats
const updateInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(updateInterval);
@@ -1465,27 +1515,31 @@ class bitvidApp {
return video;
}
// 2) Bulk fetch from relays
const allFromBulk = await nostrClient.fetchVideos();
// 2a) Deduplicate so we only keep newest version per root
const newestPerRoot = dedupeToNewestByRoot(allFromBulk);
// 2b) Find the requested ID within the deduplicated set
video = newestPerRoot.find((v) => v.id === eventId);
if (video) {
// Store it in our local map, so we can open it instantly next time
this.videosMap.set(video.id, video);
return video;
// 2) Already in nostrClient.allEvents?
// (assuming nostrClient.allEvents is a Map of id => video)
const fromAll = nostrClient.allEvents.get(eventId);
if (fromAll && !fromAll.deleted) {
this.videosMap.set(eventId, fromAll);
return fromAll;
}
// 3) Final fallback: direct single-event fetch
// 3) Direct single-event fetch (fewer resources than full fetchVideos)
const single = await nostrClient.getEventById(eventId);
if (single && !single.deleted) {
this.videosMap.set(single.id, single);
return single;
}
// 4) If you wanted a final fallback, you could do it here:
// But it's typically better to avoid repeated full fetches
// console.log("Falling back to full fetchVideos...");
// const allFetched = await nostrClient.fetchVideos();
// video = allFetched.find(v => v.id === eventId && !v.deleted);
// if (video) {
// this.videosMap.set(video.id, video);
// return video;
// }
// Not found or was deleted
return null;
}

View File

@@ -1,5 +1,6 @@
// js/lists.js
// Whitelist of npubs that can access the video upload functions
const npubs = [
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe", // bitvid
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx", // thePR0M3TH3AN
@@ -10,9 +11,14 @@ const npubs = [
"npub19ma2w9dmk3kat0nt0k5dwuqzvmg3va9ezwup0zkakhpwv0vcwvcsg8axkl", // vinney
"npub1rcr8h76csgzhdhea4a7tq5w5gydcpg9clgf0cffu6z45rnc6yp5sj7cfuz", // djmeistro
"npub1m5s9w4t03znyetxswhgq0ud7fq8ef8y3l4kscn2e8wkvmv42hh3qujgjl3", // mister_monster
"npub13qexjtmajssuhz8gdchgx65dwsnr705drse294zz5vt4e78ya2vqzyg8lv", // SatoshiSignal
];
console.log("DEBUG: lists.js loaded, npubs:", npubs);
// Blacklist of npubs that events will not be displayed in the bitvid official client
export const initialWhitelist = npubs;
export const initialBlacklist = [""];
// Block specific events with the nevent
export const initialEventBlacklist = [""];

View File

@@ -3,15 +3,18 @@
import { isDevMode } from "./config.js";
import { accessControl } from "./accessControl.js";
/**
* The usual relays
*/
const RELAY_URLS = [
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.snort.social",
"wss://nostr.wine",
"wss://relay.primal.net",
"wss://relay.nostr.band",
];
// Just a helper to keep error spam in check
// To limit error spam
let errorLogCount = 0;
const MAX_ERROR_LOGS = 100;
function logErrorOnce(message, eventContent = null) {
@@ -31,7 +34,7 @@ function logErrorOnce(message, eventContent = null) {
/**
* Example "encryption" that just reverses strings.
* In real usage, swap with actual crypto.
* In real usage, replace with actual crypto.
*/
function fakeEncrypt(magnet) {
return magnet.split("").reverse().join("");
@@ -42,39 +45,61 @@ function fakeDecrypt(encrypted) {
/**
* Convert a raw Nostr event => your "video" object.
* CHANGED: skip if version <2
*/
function convertEventToVideo(event) {
const content = JSON.parse(event.content || "{}");
return {
id: event.id,
// If content.videoRootId is missing, use event.id as a fallback
videoRootId: content.videoRootId || event.id,
version: content.version ?? 1,
isPrivate: content.isPrivate ?? false,
title: content.title || "",
magnet: content.magnet || "",
thumbnail: content.thumbnail || "",
description: content.description || "",
mode: content.mode || "live",
deleted: content.deleted === true,
pubkey: event.pubkey,
created_at: event.created_at,
tags: event.tags,
};
try {
const content = JSON.parse(event.content || "{}");
// Example checks:
const isSupportedVersion = content.version >= 2;
const hasRequiredFields = !!(content.title && content.magnet);
if (!isSupportedVersion) {
return {
id: event.id,
invalid: true,
reason: "version <2",
};
}
if (!hasRequiredFields) {
return {
id: event.id,
invalid: true,
reason: "missing title/magnet",
};
}
return {
id: event.id,
videoRootId: content.videoRootId || event.id,
version: content.version,
isPrivate: content.isPrivate ?? false,
title: content.title ?? "",
magnet: content.magnet ?? "",
thumbnail: content.thumbnail ?? "",
description: content.description ?? "",
mode: content.mode ?? "live",
deleted: content.deleted === true,
pubkey: event.pubkey,
created_at: event.created_at,
tags: event.tags,
invalid: false,
};
} catch (err) {
// JSON parse error
return { id: event.id, invalid: true, reason: "json parse error" };
}
}
/**
* Key each "active" video by its root ID => so you only store
* the newest version for each root. But for older events w/o videoRootId,
* or w/o 'd' tag, we handle fallback logic below.
* If the video has videoRootId => use that as the “group key”.
* Otherwise fallback to (pubkey + dTag), or if no dTag => “LEGACY:id”
*/
function getActiveKey(video) {
// If it has a videoRootId, we use that
if (video.videoRootId) {
return `ROOT:${video.videoRootId}`;
}
// Otherwise fallback to (pubkey + dTag) or if no dTag, fallback to event.id
// This is a fallback approach so older events appear in the "active map".
const dTag = video.tags?.find((t) => t[0] === "d");
if (dTag) {
return `${video.pubkey}:${dTag[1]}`;
@@ -88,15 +113,15 @@ class NostrClient {
this.pubkey = null;
this.relays = RELAY_URLS;
// All events—old or new—so older share links still work
// Store all events so older links still work
this.allEvents = new Map();
// "activeMap" holds only the newest version for each root ID (or fallback).
// activeMap holds only the newest version for each root
this.activeMap = new Map();
}
/**
* Connect to all configured relays
* Connect to the configured relays
*/
async init() {
if (isDevMode) console.log("Connecting to relays...");
@@ -107,7 +132,9 @@ class NostrClient {
const successfulRelays = results
.filter((r) => r.success)
.map((r) => r.url);
if (successfulRelays.length === 0) throw new Error("No relays connected");
if (successfulRelays.length === 0) {
throw new Error("No relays connected");
}
if (isDevMode) {
console.log(`Connected to ${successfulRelays.length} relay(s)`);
}
@@ -133,7 +160,6 @@ class NostrClient {
sub.unsub();
resolve({ url, success: true });
};
sub.on("event", succeed);
sub.on("eose", succeed);
})
@@ -142,7 +168,7 @@ class NostrClient {
}
/**
* Attempt Nostr extension login or abort
* Attempt login with a Nostr extension
*/
async login() {
try {
@@ -152,7 +178,6 @@ class NostrClient {
"Please install a Nostr extension (Alby, nos2x, etc.)."
);
}
const pubkey = await window.nostr.getPublicKey();
const npub = window.NostrTools.nip19.npubEncode(pubkey);
@@ -162,8 +187,7 @@ class NostrClient {
console.log("Whitelist:", accessControl.getWhitelist());
console.log("Blacklist:", accessControl.getBlacklist());
}
// Access control check
// Access control
if (!accessControl.canAccess(npub)) {
if (accessControl.isBlacklisted(npub)) {
throw new Error("Your account has been blocked on this platform.");
@@ -171,15 +195,14 @@ class NostrClient {
throw new Error("Access restricted to whitelisted users only.");
}
}
this.pubkey = pubkey;
if (isDevMode) {
console.log("Logged in with extension. Pubkey:", this.pubkey);
}
return this.pubkey;
} catch (e) {
console.error("Login error:", e);
throw e;
} catch (err) {
console.error("Login error:", err);
throw err;
}
}
@@ -188,17 +211,9 @@ class NostrClient {
if (isDevMode) console.log("User logged out.");
}
decodeNsec(nsec) {
try {
const { data } = window.NostrTools.nip19.decode(nsec);
return data;
} catch (error) {
throw new Error("Invalid NSEC key.");
}
}
/**
* Publish a *new* video with a brand-new d tag & brand-new videoRootId
* Publish a new video
* CHANGED: Force version=2 for all new notes
*/
async publishVideo(videoData, pubkey) {
if (!pubkey) throw new Error("Not logged in to publish video.");
@@ -212,13 +227,13 @@ class NostrClient {
finalMagnet = fakeEncrypt(finalMagnet);
}
// new "videoRootId" ensures all future edits know they're from the same root
// brand-new root & d
const videoRootId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const dTagValue = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const contentObject = {
videoRootId,
version: videoData.version ?? 1,
version: 2, // forcibly set version=2
deleted: false,
isPrivate: videoData.isPrivate ?? false,
title: videoData.title || "",
@@ -258,7 +273,6 @@ class NostrClient {
}
})
);
return signedEvent;
} catch (err) {
if (isDevMode) console.error("Failed to sign/publish:", err);
@@ -270,71 +284,63 @@ class NostrClient {
* Edits a video by creating a *new event* with a brand-new d tag,
* but reuses the same videoRootId as the original.
*
* => old link remains pinned to the old event, new link is a fresh ID.
* => older version is overshadowed if your dedupe logic only shows newest.
* This version forces version=2 for the original note and uses
* lowercase comparison for public keys.
*/
async editVideo(originalEventStub, updatedData, pubkey) {
if (!pubkey) {
async editVideo(originalEventStub, updatedData, userPubkey) {
if (!userPubkey) {
throw new Error("Not logged in to edit.");
}
if (!originalEventStub.pubkey || originalEventStub.pubkey !== pubkey) {
// Convert the provided pubkey to lowercase
const userPubkeyLower = userPubkey.toLowerCase();
// Use getEventById to fetch the full original event details
const baseEvent = await this.getEventById(originalEventStub.id);
if (!baseEvent) {
throw new Error("Could not retrieve the original event to edit.");
}
// Check that the original event is version 2 or higher
if (baseEvent.version < 2) {
throw new Error(
"This video is not in the supported version for editing."
);
}
// Ownership check (compare lowercase hex public keys)
if (
!baseEvent.pubkey ||
baseEvent.pubkey.toLowerCase() !== userPubkeyLower
) {
throw new Error("You do not own this video (pubkey mismatch).");
}
// 1) Attempt to get the FULL old event details (especially videoRootId)
let baseEvent = originalEventStub;
// If the caller didn't pass .videoRootId, fetch from local or relay:
if (!baseEvent.videoRootId) {
const fetched = await this.getEventById(originalEventStub.id);
if (!fetched) {
throw new Error("Could not retrieve the original event to edit.");
}
baseEvent = fetched;
}
// 2) We now have baseEvent.videoRootId if it existed
let oldRootId = baseEvent.videoRootId || null;
// Decrypt the old magnet if it was private
// Decrypt the old magnet if the note is private
let oldPlainMagnet = baseEvent.magnet || "";
if (baseEvent.isPrivate && oldPlainMagnet) {
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
}
// 3) Decide new privacy
// Determine if the updated note should be private
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
// 4) Fallback to old magnet if none was provided
let finalPlainMagnet = (updatedData.magnet || "").trim();
if (!finalPlainMagnet) {
finalPlainMagnet = oldPlainMagnet;
}
// Use the new magnet if provided; otherwise, fall back to the decrypted old magnet
let finalPlainMagnet = (updatedData.magnet || "").trim() || oldPlainMagnet;
let finalMagnet = wantPrivate
? fakeEncrypt(finalPlainMagnet)
: finalPlainMagnet;
// 5) Re-encrypt if user wants private
let finalMagnet = finalPlainMagnet;
if (wantPrivate) {
finalMagnet = fakeEncrypt(finalPlainMagnet);
}
// Use the existing videoRootId (or fall back to the base event's ID)
const oldRootId = baseEvent.videoRootId || baseEvent.id;
// 6) If there's no root yet (legacy), use the old event's own ID.
// Otherwise keep the existing rootId.
if (!oldRootId) {
oldRootId = baseEvent.id;
if (isDevMode) {
console.log(
"No existing root => using baseEvent.id as root:",
oldRootId
);
}
}
// Generate a brand-new d-tag so it doesn't overshadow the old share link
// Generate a new d-tag so that the edit gets its own share link
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
// 7) Build updated content
// Build the updated content object
const contentObject = {
videoRootId: oldRootId,
version: updatedData.version ?? baseEvent.version ?? 1,
version: updatedData.version ?? baseEvent.version ?? 2,
deleted: false,
isPrivate: wantPrivate,
title: updatedData.title ?? baseEvent.title,
@@ -346,11 +352,12 @@ class NostrClient {
const event = {
kind: 30078,
pubkey,
// Use the provided userPubkey (or you can also force it to lowercase here if desired)
pubkey: userPubkeyLower,
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "video"],
["d", newD], // new share link
["d", newD], // new share link tag
],
content: JSON.stringify(contentObject),
};
@@ -360,7 +367,6 @@ class NostrClient {
console.log("Event content:", event.content);
}
// 8) Sign and publish the new event
try {
const signedEvent = await window.nostr.signEvent(event);
if (isDevMode) {
@@ -381,6 +387,7 @@ class NostrClient {
}
})
);
return signedEvent;
} catch (err) {
console.error("Edit failed:", err);
@@ -389,7 +396,7 @@ class NostrClient {
}
/**
* "Reverting" => we just mark the most recent content as {deleted:true} and blank out magnet/desc
* revertVideo => old style
*/
async revertVideo(originalEvent, pubkey) {
if (!pubkey) {
@@ -399,7 +406,6 @@ class NostrClient {
throw new Error("Not your event (pubkey mismatch).");
}
// If front-end didn't pass the tags array, load the full event:
let baseEvent = originalEvent;
if (!baseEvent.tags || !Array.isArray(baseEvent.tags)) {
const fetched = await this.getEventById(originalEvent.id);
@@ -423,7 +429,6 @@ class NostrClient {
};
}
// Check d-tag
const dTag = baseEvent.tags.find((t) => t[0] === "d");
if (!dTag) {
throw new Error(
@@ -435,17 +440,15 @@ class NostrClient {
const oldContent = JSON.parse(baseEvent.content || "{}");
const oldVersion = oldContent.version ?? 1;
// If no root, fallback
let finalRootId = oldContent.videoRootId || null;
if (!finalRootId) {
finalRootId = `LEGACY:${baseEvent.pubkey}:${existingD}`;
}
// Build “deleted: true” overshadow event => revert current version
const contentObject = {
videoRootId: finalRootId,
version: oldVersion,
deleted: true, // mark *this version* as deleted
deleted: true,
isPrivate: oldContent.isPrivate ?? false,
title: oldContent.title || "",
magnet: "",
@@ -460,7 +463,7 @@ class NostrClient {
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "video"],
["d", existingD], // re-use same d => overshadow
["d", existingD],
],
content: JSON.stringify(contentObject),
};
@@ -471,9 +474,7 @@ class NostrClient {
try {
await this.pool.publish([url], signedEvent);
} catch (err) {
if (isDevMode) {
console.error(`Failed to revert on ${url}`, err);
}
if (isDevMode) console.error(`Failed to revert on ${url}`, err);
}
})
);
@@ -482,14 +483,26 @@ class NostrClient {
}
/**
* "Deleting" => we just mark all content with the same videoRootId as {deleted:true} and blank out magnet/desc
* "Deleting" => Mark all content with the same videoRootId as {deleted:true}
* and blank out magnet/desc.
*
* This version now asks for confirmation before proceeding.
*/
async deleteAllVersions(videoRootId, pubkey) {
if (!pubkey) {
throw new Error("Not logged in to delete all versions.");
}
// Ask for confirmation before proceeding
if (
!window.confirm(
"Are you sure you want to delete all versions of this video? This action cannot be undone."
)
) {
console.log("Deletion cancelled by user.");
return null; // Cancel deletion if user clicks "Cancel"
}
// 1) Find all events in our local allEvents that share the same root.
const matchingEvents = [];
for (const [id, vid] of this.allEvents.entries()) {
@@ -501,19 +514,15 @@ class NostrClient {
matchingEvents.push(vid);
}
}
// If you want to re-check the relay for older versions too,
// you can do a fallback query, but typically your local cache is enough.
if (!matchingEvents.length) {
throw new Error("No existing events found for that root.");
}
// 2) For each event, create a "deleted: true" overshadow
// by re-using the same d-tag so it cannot appear again.
// 2) For each event, create a "revert" event to mark it as deleted.
// This will prompt the user (via the extension) to sign the deletion.
for (const vid of matchingEvents) {
await this.revertVideo(
{
// re-using revertVideo logic
id: vid.id,
pubkey: vid.pubkey,
content: JSON.stringify({
@@ -532,19 +541,22 @@ class NostrClient {
);
}
// Optionally return some status
return true;
}
/**
* Subscribes to *all* video events. We store them in this.allEvents so older
* notes remain accessible by ID, plus we maintain this.activeMap for the newest
* version of each root (or fallback).
* subscribeVideos => old approach
*/
/**
* Subscribe to *all* videos (old and new) with a single subscription,
* then call onVideo() each time a new or updated event arrives.
*/
subscribeVideos(onVideo) {
const filter = {
kinds: [30078],
"#t": ["video"],
// Remove or adjust limit if you prefer,
// and set since=0 to retrieve historical events:
limit: 500,
since: 0,
};
@@ -553,37 +565,31 @@ class NostrClient {
}
const sub = this.pool.sub(this.relays, [filter]);
const invalidDuringSub = [];
sub.on("event", (event) => {
try {
const video = convertEventToVideo(event);
if (video.invalid) {
invalidDuringSub.push({ id: video.id, reason: video.reason });
return;
}
// Store in allEvents
this.allEvents.set(event.id, video);
// If its marked deleted, remove from active map if its the active version
// NEW CODE
// If it's a "deleted" note, remove from activeMap
if (video.deleted) {
const activeKey = getActiveKey(video);
// Don't compare IDs—just remove that key from the active map
this.activeMap.delete(activeKey);
// (Optional) If you want a debug log:
// console.log(`[DELETE] Removed activeKey=${activeKey}`);
return;
}
// Not deleted => see if its the newest
// Otherwise, if it's newer than what we have, update activeMap
const activeKey = getActiveKey(video);
const prevActive = this.activeMap.get(activeKey);
if (!prevActive) {
// brand new => set it
if (!prevActive || video.created_at > prevActive.created_at) {
this.activeMap.set(activeKey, video);
onVideo(video);
} else {
// compare timestamps
if (video.created_at > prevActive.created_at) {
this.activeMap.set(activeKey, video);
onVideo(video);
}
onVideo(video); // trigger the callback that re-renders
}
} catch (err) {
if (isDevMode) {
@@ -593,8 +599,16 @@ class NostrClient {
});
sub.on("eose", () => {
if (isDevMode && invalidDuringSub.length > 0) {
console.warn(
`[subscribeVideos] found ${invalidDuringSub.length} invalid v2 notes:`,
invalidDuringSub
);
}
if (isDevMode) {
console.log("[subscribeVideos] Reached EOSE for all relays");
console.log(
"[subscribeVideos] Reached EOSE for all relays (historical load done)"
);
}
});
@@ -602,7 +616,7 @@ class NostrClient {
}
/**
* Bulk fetch from all relays, store in allEvents, rebuild activeMap
* fetchVideos => old approach
*/
async fetchVideos() {
const filter = {
@@ -613,39 +627,51 @@ class NostrClient {
};
const localAll = new Map();
// NEW: track invalid
const invalidNotes = [];
try {
// 1) Fetch all events from each relay
await Promise.all(
this.relays.map(async (url) => {
const events = await this.pool.list([url], [filter]);
for (const evt of events) {
const vid = convertEventToVideo(evt);
localAll.set(evt.id, vid);
if (vid.invalid) {
// Accumulate if invalid
invalidNotes.push({ id: vid.id, reason: vid.reason });
} else {
// Only add if good
localAll.set(evt.id, vid);
}
}
})
);
// 2) Merge into this.allEvents
// Merge into allEvents
for (const [id, vid] of localAll.entries()) {
this.allEvents.set(id, vid);
}
// 3) Rebuild activeMap
// Rebuild activeMap
this.activeMap.clear();
for (const [id, video] of this.allEvents.entries()) {
// Skip if the video is marked deleted
if (video.deleted) continue;
const activeKey = getActiveKey(video);
const existing = this.activeMap.get(activeKey);
// If there's no existing entry or this is newer, set/replace
if (!existing || video.created_at > existing.created_at) {
this.activeMap.set(activeKey, video);
}
}
// 4) Return newest version for each root in descending order
// OPTIONAL: Log invalid stats
if (invalidNotes.length > 0 && isDevMode) {
console.warn(
`Skipped ${invalidNotes.length} invalid v2 notes:\n`,
invalidNotes.map((n) => `${n.id.slice(0, 8)}.. => ${n.reason}`)
);
}
const activeVideos = Array.from(this.activeMap.values()).sort(
(a, b) => b.created_at - a.created_at
);
@@ -657,14 +683,13 @@ class NostrClient {
}
/**
* Attempt to fetch an event by ID from local cache, then from the relays
* getEventById => old approach
*/
async getEventById(eventId) {
const local = this.allEvents.get(eventId);
if (local) {
return local;
}
// direct fetch if missing
try {
for (const url of this.relays) {
const maybeEvt = await this.pool.get([url], { ids: [eventId] });
@@ -679,12 +704,9 @@ class NostrClient {
console.error("getEventById direct fetch error:", err);
}
}
return null; // not found
return null;
}
/**
* Return newest versions from activeMap if you want to skip older events
*/
getActiveVideos() {
return Array.from(this.activeMap.values()).sort(
(a, b) => b.created_at - a.created_at

View File

@@ -1,5 +1,3 @@
// js/webtorrent.js
import WebTorrent from "./webtorrent.min.js";
export class TorrentClient {
@@ -7,8 +5,6 @@ export class TorrentClient {
this.client = new WebTorrent();
this.currentTorrent = null;
this.TIMEOUT_DURATION = 60000; // 60 seconds
// We remove the “statsInterval” since were not using it here anymore
// this.statsInterval = null;
}
log(msg) {
@@ -71,7 +67,6 @@ export class TorrentClient {
throw new Error("Service Worker not supported or disabled");
}
// Optional Brave config
if (isBraveBrowser) {
this.log("Checking Brave configuration...");
if (!navigator.serviceWorker) {
@@ -84,23 +79,17 @@ export class TorrentClient {
}
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
await registration.unregister();
for (const reg of registrations) {
await reg.unregister();
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
const currentPath = window.location.pathname;
const basePath = currentPath.substring(
0,
currentPath.lastIndexOf("/") + 1
);
this.log("Registering service worker...");
this.log("Registering service worker at /sw.min.js...");
const registration = await navigator.serviceWorker.register(
"./sw.min.js",
{
scope: basePath,
scope: "./",
updateViaCache: "none",
}
);
@@ -143,6 +132,9 @@ export class TorrentClient {
throw new Error("Service worker not active after ready state");
}
// Force the SW to check for updates
registration.update();
this.log("Service worker ready");
return registration;
} catch (error) {
@@ -151,83 +143,51 @@ export class TorrentClient {
}
}
formatBytes(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
}
/**
* Streams the magnet to the <video> element.
* No stats intervals here—just returns the torrent object.
*/
async streamVideo(magnetURI, videoElement) {
try {
// 1) Setup service worker
const registration = await this.setupServiceWorker();
if (!registration || !registration.active) {
throw new Error("Service worker setup failed");
}
// 2) Create WebTorrent server
this.client.createServer({ controller: registration });
this.log("WebTorrent server created");
const isFirefoxBrowser = this.isFirefox();
return new Promise((resolve, reject) => {
if (isFirefoxBrowser) {
this.log("Starting torrent download (Firefox path)");
this.client.add(
magnetURI,
{ strategy: "sequential", maxWebConns: 4 },
(torrent) => {
this.log("Torrent added (Firefox path):", torrent.name);
this.handleFirefoxTorrent(torrent, videoElement, resolve, reject);
}
);
} else {
this.log("Starting torrent download (Chrome path)");
this.client.add(magnetURI, (torrent) => {
this.log("Torrent added (Chrome path):", torrent.name);
this.handleChromeTorrent(torrent, videoElement, resolve, reject);
});
}
});
} catch (error) {
this.log("Failed to setup video streaming:", error);
throw error;
}
}
// Minimal handleChromeTorrent — no internal setInterval
// Minimal handleChromeTorrent
handleChromeTorrent(torrent, videoElement, resolve, reject) {
const file = torrent.files.find((f) =>
/\.(mp4|webm|mkv)$/.test(f.name.toLowerCase())
);
torrent.on("warning", (err) => {
if (err && typeof err.message === "string") {
if (
err.message.includes("CORS") ||
err.message.includes("Access-Control-Allow-Origin")
) {
console.warn(
"CORS warning detected. Attempting to remove the failing webseed/tracker."
);
if (torrent._opts?.urlList?.length) {
torrent._opts.urlList = torrent._opts.urlList.filter((url) => {
return !url.includes("distribution.bbb3d.renderfarming.net");
});
console.warn("Cleaned up webseeds =>", torrent._opts.urlList);
}
if (torrent._opts?.announce?.length) {
torrent._opts.announce = torrent._opts.announce.filter((url) => {
return !url.includes("fastcast.nz");
});
console.warn("Cleaned up trackers =>", torrent._opts.announce);
}
}
}
});
const file = torrent.files.find((f) => /\.(mp4|webm|mkv)$/i.test(f.name));
if (!file) {
return reject(new Error("No compatible video file found in torrent"));
}
// Mute & crossOrigin
videoElement.muted = true;
videoElement.crossOrigin = "anonymous";
// Catch video errors
videoElement.addEventListener("error", (e) => {
this.log("Video error:", e.target.error);
});
// Attempt autoplay
videoElement.addEventListener("canplay", () => {
videoElement.play().catch((err) => {
this.log("Autoplay failed:", err);
});
});
// Actually stream
try {
file.streamTo(videoElement);
this.currentTorrent = torrent;
@@ -237,7 +197,6 @@ export class TorrentClient {
reject(err);
}
// Also handle torrent error events
torrent.on("error", (err) => {
this.log("Torrent error (Chrome path):", err);
reject(err);
@@ -282,11 +241,57 @@ export class TorrentClient {
}
/**
* Clean up
* Initiates streaming of a torrent magnet to a <video> element.
* Ensures the service worker is registered first.
*/
async streamVideo(magnetURI, videoElement) {
try {
// 1) Setup service worker
const registration = await this.setupServiceWorker();
if (!registration || !registration.active) {
throw new Error("Service worker setup failed");
}
// Create the WebTorrent server with the registered service worker.
// Force the server to use '/webtorrent' as the URL prefix.
this.client.createServer({
controller: registration,
pathPrefix: "/webtorrent",
});
this.log("WebTorrent server created");
const isFirefoxBrowser = this.isFirefox();
return new Promise((resolve, reject) => {
if (isFirefoxBrowser) {
this.log("Starting torrent download (Firefox path)");
this.client.add(
magnetURI,
{ strategy: "sequential", maxWebConns: 4 },
(torrent) => {
this.log("Torrent added (Firefox path):", torrent.name);
this.handleFirefoxTorrent(torrent, videoElement, resolve, reject);
}
);
} else {
this.log("Starting torrent download (Chrome path)");
this.client.add(magnetURI, (torrent) => {
this.log("Torrent added (Chrome path):", torrent.name);
this.handleChromeTorrent(torrent, videoElement, resolve, reject);
});
}
});
} catch (error) {
this.log("Failed to setup video streaming:", error);
throw error;
}
}
/**
* Clean up resources.
*/
async cleanup() {
try {
// No local interval to clear here
if (this.currentTorrent) {
this.currentTorrent.destroy();
}

View File

@@ -29,7 +29,7 @@
"type": "image/png"
}
],
"start_url": "/index.html",
"start_url": "index.html",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#0f172a",

277
src/sw.min.js vendored
View File

@@ -1,132 +1,161 @@
(() => {
"use strict";
let cancelled = false;
"use strict";
// Handle skip waiting message
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
let cancelled = false;
// Handle messages from clients
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
if (event.data && event.data.type === "CLEAR_CACHES") {
caches
.keys()
.then((cacheNames) =>
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
);
}
});
// Immediately install and skip waiting
self.addEventListener("install", (event) => {
self.skipWaiting();
});
// Claim clients on activation and clear caches
self.addEventListener("activate", (event) => {
event.waitUntil(
Promise.all([
clients.claim(),
self.skipWaiting(),
caches
.keys()
.then((cacheNames) =>
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
),
])
);
});
// Handle fetch events
self.addEventListener("fetch", (event) => {
const requestURL = event.request.url;
// Only handle WebTorrent streaming requests; let other requests proceed normally.
if (!requestURL.includes("/webtorrent/")) {
return;
}
const responsePromise = (async () => {
// Handle keepalive requests
if (requestURL.includes("/webtorrent/keepalive/")) {
return new Response();
}
// Handle cancel requests
if (requestURL.includes("/webtorrent/cancel/")) {
return new Response(
new ReadableStream({
cancel() {
cancelled = true;
},
})
);
}
// Handle streaming requests
return (async function ({ request }) {
const { url, method, headers, destination } = request;
const windowClients = await clients.matchAll({
type: "window",
includeUncontrolled: true,
});
const [clientResponse, port] = await new Promise((resolve) => {
for (const client of windowClients) {
const channel = new MessageChannel();
channel.port1.onmessage = ({ data }) => {
resolve([data, channel.port1]);
};
client.postMessage(
{
url,
method,
headers: Object.fromEntries(headers.entries()),
scope: self.registration.scope,
destination,
type: "webtorrent",
},
[channel.port2]
);
}
});
let timeoutId = null;
const closeChannel = () => {
port.postMessage(false);
clearTimeout(timeoutId);
port.onmessage = null;
};
// Clone and update headers to prevent caching.
const responseHeaders = new Headers(clientResponse.headers);
responseHeaders.set(
"Cache-Control",
"no-cache, no-store, must-revalidate, max-age=0"
);
responseHeaders.set("Pragma", "no-cache");
responseHeaders.set("Expires", "0");
// If the response is not a streaming request, return it directly.
if (clientResponse.body !== "STREAM") {
closeChannel();
return new Response(clientResponse.body, {
status: clientResponse.status,
statusText: clientResponse.statusText,
headers: responseHeaders,
});
}
})
// Immediately install and activate
self.addEventListener("install", () => {
self.skipWaiting()
})
// Claim clients on activation
self.addEventListener('activate', event => {
event.waitUntil(
Promise.all([
clients.claim(),
self.skipWaiting(),
caches.keys().then(cacheNames =>
Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)))
)
])
)
})
// Handle fetch events
self.addEventListener("fetch", s => {
const t = (s => {
const { url: t } = s.request;
// Only handle webtorrent requests
if (!t.includes(self.registration.scope + "webtorrent/")) {
return null;
}
// Handle keepalive requests
if (t.includes(self.registration.scope + "webtorrent/keepalive/")) {
return new Response();
}
// Handle cancel requests
if (t.includes(self.registration.scope + "webtorrent/cancel/")) {
return new Response(new ReadableStream({
cancel() {
cancelled = true;
}
}));
}
// Handle streaming requests
return async function({ request: s }) {
const { url: t, method: n, headers: o, destination: a } = s;
// Get all window clients
const l = await clients.matchAll({
type: "window",
includeUncontrolled: true
});
// Create message channel and wait for response
const [r, i] = await new Promise(e => {
for (const s of l) {
const l = new MessageChannel,
{ port1: r, port2: i } = l;
r.onmessage = ({ data: s }) => {
e([s, r])
};
s.postMessage({
url: t,
method: n,
headers: Object.fromEntries(o.entries()),
scope: self.registration.scope,
destination: a,
type: "webtorrent"
}, [i]);
}
});
let c = null;
const d = () => {
i.postMessage(false);
clearTimeout(c);
i.onmessage = null;
// Otherwise, stream the response via a ReadableStream.
return new Response(
new ReadableStream({
pull(controller) {
return new Promise((resolvePull) => {
port.onmessage = ({ data }) => {
if (data) {
controller.enqueue(data);
} else {
closeChannel();
controller.close();
}
resolvePull();
};
// Handle non-streaming response
if (r.body !== "STREAM") {
d();
return new Response(r.body, r);
if (!cancelled && destination !== "document") {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
closeChannel();
resolvePull();
}, 5000);
}
// Handle streaming response
return new Response(new ReadableStream({
pull: s => new Promise(t => {
i.onmessage = ({ data: e }) => {
if (e) {
s.enqueue(e);
} else {
d();
s.close();
}
t();
};
if (!cancelled && a !== "document") {
clearTimeout(c);
c = setTimeout(() => {
d();
t();
}, 5000);
}
i.postMessage(true);
}),
cancel() {
d();
}
}), r);
}(s);
})(s);
if (t) {
s.respondWith(t);
}
});
})();
port.postMessage(true);
});
},
cancel() {
closeChannel();
},
}),
{
status: clientResponse.status,
statusText: clientResponse.statusText,
headers: responseHeaders,
}
);
})(event);
})();
if (responsePromise) {
event.respondWith(responsePromise);
}
});
})();