mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 03:12:31 +00:00
Merge pull request #1001 from PR0M3TH3AN/codex/update-mime-handling-for-nip-94-compliance
Normalize NIP-94 MIME handling
This commit is contained in:
5 files changed
+180
-40
No files matched your search
@@ -152,6 +152,8 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
|
||||
| Admin blacklist (`NOTE_TYPES.ADMIN_BLACKLIST`) | `30000` | `['d', 'bitvid:admin:blacklist']`, repeated `['p', <pubkey>]` entries | Empty content |
|
||||
| Admin whitelist (`NOTE_TYPES.ADMIN_WHITELIST`) | `30000` | `['d', 'bitvid:admin:whitelist']`, repeated `['p', <pubkey>]` entries | Empty content |
|
||||
|
||||
NIP-94 compliance: mirror events now normalize the `['m', <mime>]` tag to lowercase—covering both user input and inferred values—so they satisfy [NIP-94's lowercase MIME requirement](./nips/94.md). Related helpers reuse the same normalization to keep future publishers aligned.
|
||||
|
||||
### Direct messages
|
||||
|
||||
bitvid now consumes both legacy direct messages (kind `4`) and modern gift-wrap envelopes (kind `1059`). The `js/dmDecryptor.js` helper unwraps these events by:
|
||||
|
||||
+32
-21
@@ -694,23 +694,30 @@ function buildDmFilters(actorPubkey, { since, until, limit } = {}) {
|
||||
}
|
||||
|
||||
|
||||
const EXTENSION_MIME_MAP = {
|
||||
mp4: "video/mp4",
|
||||
m4v: "video/x-m4v",
|
||||
webm: "video/webm",
|
||||
mkv: "video/x-matroska",
|
||||
mov: "video/quicktime",
|
||||
avi: "video/x-msvideo",
|
||||
ogv: "video/ogg",
|
||||
ogg: "video/ogg",
|
||||
m3u8: "application/x-mpegURL",
|
||||
mpd: "application/dash+xml",
|
||||
ts: "video/mp2t",
|
||||
mpg: "video/mpeg",
|
||||
mpeg: "video/mpeg",
|
||||
flv: "video/x-flv",
|
||||
"3gp": "video/3gpp",
|
||||
};
|
||||
const EXTENSION_MIME_MAP = Object.freeze(
|
||||
Object.fromEntries(
|
||||
Object.entries({
|
||||
mp4: "video/mp4",
|
||||
m4v: "video/x-m4v",
|
||||
webm: "video/webm",
|
||||
mkv: "video/x-matroska",
|
||||
mov: "video/quicktime",
|
||||
avi: "video/x-msvideo",
|
||||
ogv: "video/ogg",
|
||||
ogg: "video/ogg",
|
||||
m3u8: "application/x-mpegurl",
|
||||
mpd: "application/dash+xml",
|
||||
ts: "video/mp2t",
|
||||
mpg: "video/mpeg",
|
||||
mpeg: "video/mpeg",
|
||||
flv: "video/x-flv",
|
||||
"3gp": "video/3gpp",
|
||||
}).map(([extension, mimeType]) => [
|
||||
extension,
|
||||
typeof mimeType === "string" ? mimeType.toLowerCase() : "",
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
function inferMimeTypeFromUrl(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
@@ -733,7 +740,8 @@ function inferMimeTypeFromUrl(url) {
|
||||
}
|
||||
|
||||
const extension = match[1].toLowerCase();
|
||||
return EXTENSION_MIME_MAP[extension] || "";
|
||||
const mimeType = EXTENSION_MIME_MAP[extension];
|
||||
return typeof mimeType === "string" ? mimeType : "";
|
||||
}
|
||||
|
||||
function getActiveKey(video) {
|
||||
@@ -3932,7 +3940,7 @@ export class NostrClient {
|
||||
typeof videoData.title === "string" ? videoData.title.trim() : "";
|
||||
const providedMimeType =
|
||||
typeof videoData.mimeType === "string"
|
||||
? videoData.mimeType.trim()
|
||||
? videoData.mimeType.trim().toLowerCase()
|
||||
: "";
|
||||
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
@@ -4000,8 +4008,11 @@ export class NostrClient {
|
||||
|
||||
if (finalUrl) {
|
||||
const inferredMimeType = inferMimeTypeFromUrl(finalUrl);
|
||||
const mimeType =
|
||||
providedMimeType || inferredMimeType || "application/octet-stream";
|
||||
const mimeTypeSource =
|
||||
providedMimeType ||
|
||||
inferredMimeType ||
|
||||
"application/octet-stream";
|
||||
const mimeType = mimeTypeSource.toLowerCase();
|
||||
|
||||
const mirrorTags = [
|
||||
["url", finalUrl],
|
||||
|
||||
+26
-18
@@ -1174,23 +1174,30 @@ export async function populateNip71MetadataForVideos(videos = [], {
|
||||
});
|
||||
}
|
||||
|
||||
const EXTENSION_MIME_MAP = {
|
||||
mp4: "video/mp4",
|
||||
m4v: "video/x-m4v",
|
||||
webm: "video/webm",
|
||||
mkv: "video/x-matroska",
|
||||
mov: "video/quicktime",
|
||||
avi: "video/x-msvideo",
|
||||
ogv: "video/ogg",
|
||||
ogg: "video/ogg",
|
||||
m3u8: "application/x-mpegURL",
|
||||
mpd: "application/dash+xml",
|
||||
ts: "video/mp2t",
|
||||
mpg: "video/mpeg",
|
||||
mpeg: "video/mpeg",
|
||||
flv: "video/x-flv",
|
||||
"3gp": "video/3gpp",
|
||||
};
|
||||
const EXTENSION_MIME_MAP = Object.freeze(
|
||||
Object.fromEntries(
|
||||
Object.entries({
|
||||
mp4: "video/mp4",
|
||||
m4v: "video/x-m4v",
|
||||
webm: "video/webm",
|
||||
mkv: "video/x-matroska",
|
||||
mov: "video/quicktime",
|
||||
avi: "video/x-msvideo",
|
||||
ogv: "video/ogg",
|
||||
ogg: "video/ogg",
|
||||
m3u8: "application/x-mpegurl",
|
||||
mpd: "application/dash+xml",
|
||||
ts: "video/mp2t",
|
||||
mpg: "video/mpeg",
|
||||
mpeg: "video/mpeg",
|
||||
flv: "video/x-flv",
|
||||
"3gp": "video/3gpp",
|
||||
}).map(([extension, mimeType]) => [
|
||||
extension,
|
||||
typeof mimeType === "string" ? mimeType.toLowerCase() : "",
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
function inferMimeTypeFromUrl(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
@@ -1213,7 +1220,8 @@ function inferMimeTypeFromUrl(url) {
|
||||
}
|
||||
|
||||
const extension = match[1].toLowerCase();
|
||||
return EXTENSION_MIME_MAP[extension] || "";
|
||||
const mimeType = EXTENSION_MIME_MAP[extension];
|
||||
return typeof mimeType === "string" ? mimeType : "";
|
||||
}
|
||||
|
||||
export function convertEventToVideo(event = {}) {
|
||||
|
||||
@@ -791,10 +791,20 @@ export async function mirrorVideoEvent({
|
||||
}
|
||||
|
||||
const providedMimeType = sanitize(options.mimeType);
|
||||
const normalizedProvidedMimeType = providedMimeType
|
||||
? providedMimeType.toLowerCase()
|
||||
: "";
|
||||
const inferredMimeType = inferMimeTypeFromUrl
|
||||
? inferMimeTypeFromUrl(url)
|
||||
: "";
|
||||
const mimeType = providedMimeType || inferredMimeType || "application/octet-stream";
|
||||
const normalizedInferredMimeType = inferredMimeType
|
||||
? inferredMimeType.toLowerCase()
|
||||
: "";
|
||||
const mimeTypeSource =
|
||||
normalizedProvidedMimeType ||
|
||||
normalizedInferredMimeType ||
|
||||
"application/octet-stream";
|
||||
const mimeType = mimeTypeSource.toLowerCase();
|
||||
|
||||
const explicitAlt = sanitize(options.altText);
|
||||
const altText = explicitAlt || description || title || "";
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { mirrorVideoEvent } = await import("../../js/nostr/publishHelpers.js");
|
||||
|
||||
const resolveActiveSignerStub = () => ({
|
||||
signEvent: async (event) => ({
|
||||
...event,
|
||||
id: `${event.kind}-${event.created_at}`,
|
||||
sig: "active-signature",
|
||||
}),
|
||||
});
|
||||
|
||||
const shouldRequestExtensionPermissionsStub = () => false;
|
||||
|
||||
const signEventWithPrivateKeyStub = (event) => ({
|
||||
...event,
|
||||
id: `${event.kind}-${event.created_at}-session`,
|
||||
sig: "session-signature",
|
||||
});
|
||||
|
||||
function createPublishClient({ actorPubkey = "" } = {}) {
|
||||
const publishCalls = [];
|
||||
const pool = {
|
||||
publish(urls, event) {
|
||||
publishCalls.push({ urls, event });
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
handler();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const client = {
|
||||
pubkey: actorPubkey,
|
||||
relays: ["wss://relay.example"],
|
||||
writeRelays: ["wss://relay.example"],
|
||||
pool,
|
||||
allEvents: new Map(),
|
||||
rawEvents: new Map(),
|
||||
ensurePool: async () => pool,
|
||||
ensureSessionActor: async () => actorPubkey,
|
||||
ensureExtensionPermissions: async () => ({ ok: true }),
|
||||
};
|
||||
|
||||
if (actorPubkey) {
|
||||
client.sessionActor = {
|
||||
pubkey: actorPubkey,
|
||||
privateKey: "session-private-key",
|
||||
};
|
||||
}
|
||||
|
||||
return { client, publishCalls };
|
||||
}
|
||||
|
||||
test("mirrorVideoEvent lowercases provided MIME types", async () => {
|
||||
const eventId = "provided-mime";
|
||||
const actorPubkey = "1".repeat(64);
|
||||
const { client } = createPublishClient({ actorPubkey });
|
||||
|
||||
const result = await mirrorVideoEvent({
|
||||
client,
|
||||
eventId,
|
||||
options: {
|
||||
url: "https://videos.example/demo.mp4",
|
||||
mimeType: "Video/MP4",
|
||||
},
|
||||
resolveActiveSigner: resolveActiveSignerStub,
|
||||
shouldRequestExtensionPermissions: shouldRequestExtensionPermissionsStub,
|
||||
signEventWithPrivateKey: signEventWithPrivateKeyStub,
|
||||
inferMimeTypeFromUrl: () => "",
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
const mTag = result.event?.tags?.find(
|
||||
(tag) => Array.isArray(tag) && tag[0] === "m",
|
||||
);
|
||||
assert.ok(mTag, "mirror events must include a MIME tag");
|
||||
assert.equal(mTag[1], "video/mp4");
|
||||
});
|
||||
|
||||
test("mirrorVideoEvent lowercases inferred MIME types", async () => {
|
||||
const eventId = "inferred-mime";
|
||||
const actorPubkey = "2".repeat(64);
|
||||
const { client } = createPublishClient({ actorPubkey });
|
||||
|
||||
const result = await mirrorVideoEvent({
|
||||
client,
|
||||
eventId,
|
||||
options: {
|
||||
url: "https://videos.example/demo.webm",
|
||||
},
|
||||
resolveActiveSigner: resolveActiveSignerStub,
|
||||
shouldRequestExtensionPermissions: shouldRequestExtensionPermissionsStub,
|
||||
signEventWithPrivateKey: signEventWithPrivateKeyStub,
|
||||
inferMimeTypeFromUrl: () => "VIDEO/WEBM",
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
const mTag = result.event?.tags?.find(
|
||||
(tag) => Array.isArray(tag) && tag[0] === "m",
|
||||
);
|
||||
assert.ok(mTag, "mirror events must include a MIME tag");
|
||||
assert.equal(mTag[1], "video/webm");
|
||||
});
|
||||
Reference in new issue
Block a user