Release 0.5.8
This commit is contained in:
1 parent
22ae3a68d6
commit
2851c7d936
33 files changed
+1263
-272
No files matched your search
+134
-56
@@ -1,6 +1,5 @@
|
||||
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
||||
import type Database from 'better-sqlite3';
|
||||
import { getMediaFile } from './content.js';
|
||||
import { getDatabase } from './database.js';
|
||||
import { renderMarkdown } from './markdown.js';
|
||||
import { buildEmbedUrl, fetchPresentationImage, type PresentationContent } from './presentation.js';
|
||||
@@ -414,6 +413,34 @@ export function goBack(
|
||||
);
|
||||
}
|
||||
|
||||
export function selectUsage(
|
||||
showId: string,
|
||||
usageId: string,
|
||||
commandId: string,
|
||||
expectedRevision: number,
|
||||
nowValue?: Date
|
||||
) {
|
||||
return executeCommand(
|
||||
showId,
|
||||
commandId,
|
||||
expectedRevision,
|
||||
'select',
|
||||
(database, state, now) => {
|
||||
if (!state.started_at_utc) throw new Error('NOT_STARTED');
|
||||
usageBelongsToShow(database, showId, usageId);
|
||||
if (state.current_usage_id === usageId) return;
|
||||
if (state.current_usage_id) {
|
||||
closeActiveRun(database, showId, now);
|
||||
database
|
||||
.prepare("UPDATE piece_usages SET status = 'open' WHERE id = ?")
|
||||
.run(state.current_usage_id);
|
||||
}
|
||||
activateUsage(database, showId, usageId, now);
|
||||
},
|
||||
nowValue
|
||||
);
|
||||
}
|
||||
|
||||
export function presentUsage(
|
||||
showId: string,
|
||||
usageId: string | null,
|
||||
@@ -480,25 +507,90 @@ export function issuePresentationAccess(showId: string) {
|
||||
const database = getDatabase();
|
||||
const now = nowIso();
|
||||
ensureLiveState(database, showId, now);
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
database.transaction(() => {
|
||||
database
|
||||
.prepare(
|
||||
'UPDATE presentation_access_tokens SET revoked_at_utc = ? WHERE show_id = ? AND revoked_at_utc IS NULL'
|
||||
)
|
||||
.run(now, showId);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO presentation_access_tokens (id, show_id, token_hash, created_at_utc)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
)
|
||||
.run(randomUUID(), showId, hashToken(token), now);
|
||||
return database.transaction(() => {
|
||||
let channel = database
|
||||
.prepare("SELECT * FROM presentation_channels WHERE id = 'default'")
|
||||
.get() as { access_key: string; show_id: string | null } | undefined;
|
||||
if (!channel) {
|
||||
const accessKey = randomBytes(32).toString('base64url');
|
||||
clearPresentationState(database, showId);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO presentation_channels
|
||||
(id, name, access_key, key_hash, show_id, revision, created_at_utc, updated_at_utc)
|
||||
VALUES ('default', 'Standardausgabe', ?, ?, ?, 1, ?, ?)`
|
||||
)
|
||||
.run(accessKey, hashToken(accessKey), showId, now, now);
|
||||
channel = { access_key: accessKey, show_id: showId };
|
||||
} else if (channel.show_id !== showId) {
|
||||
clearPresentationState(database, showId);
|
||||
database
|
||||
.prepare(
|
||||
`UPDATE presentation_channels SET show_id = ?, revision = revision + 1,
|
||||
updated_at_utc = ? WHERE id = 'default'`
|
||||
)
|
||||
.run(showId, now);
|
||||
}
|
||||
return channel.access_key;
|
||||
})();
|
||||
return token;
|
||||
}
|
||||
|
||||
function clearPresentationState(database: Database.Database, showId: string) {
|
||||
database
|
||||
.prepare(
|
||||
`UPDATE show_live_states SET presentation_usage_id = NULL,
|
||||
presentation_revision = presentation_revision + 1, updated_at_utc = ?
|
||||
WHERE show_id = ? AND presentation_usage_id IS NOT NULL`
|
||||
)
|
||||
.run(nowIso(), showId);
|
||||
}
|
||||
|
||||
export function clearPresentationChannel() {
|
||||
const now = nowIso();
|
||||
getDatabase().transaction(() => {
|
||||
const database = getDatabase();
|
||||
const channel = database
|
||||
.prepare("SELECT show_id FROM presentation_channels WHERE id = 'default'")
|
||||
.get() as { show_id: string | null } | undefined;
|
||||
if (!channel) throw new Error('NOT_FOUND');
|
||||
if (channel.show_id) clearPresentationState(database, channel.show_id);
|
||||
database
|
||||
.prepare(
|
||||
`UPDATE presentation_channels SET show_id = NULL, revision = revision + 1,
|
||||
updated_at_utc = ? WHERE id = 'default'`
|
||||
)
|
||||
.run(now);
|
||||
})();
|
||||
}
|
||||
|
||||
export function rotatePresentationChannelKey(showId: string) {
|
||||
issuePresentationAccess(showId);
|
||||
const accessKey = randomBytes(32).toString('base64url');
|
||||
const now = nowIso();
|
||||
getDatabase()
|
||||
.prepare(
|
||||
`UPDATE presentation_channels SET access_key = ?, key_hash = ?, revision = revision + 1,
|
||||
updated_at_utc = ? WHERE id = 'default'`
|
||||
)
|
||||
.run(accessKey, hashToken(accessKey), now);
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
export function presentationChannelAccess(channelId: string, accessKey: string) {
|
||||
if (!channelId || !accessKey || accessKey.length > 200) return false;
|
||||
return Boolean(
|
||||
getDatabase()
|
||||
.prepare('SELECT 1 FROM presentation_channels WHERE id = ? AND key_hash = ?')
|
||||
.get(channelId, hashToken(accessKey))
|
||||
);
|
||||
}
|
||||
|
||||
function presentationShowId(database: Database.Database, token: string) {
|
||||
if (!token || token.length > 200) return null;
|
||||
const channel = database
|
||||
.prepare('SELECT show_id FROM presentation_channels WHERE key_hash = ?')
|
||||
.get(hashToken(token)) as { show_id: string | null } | undefined;
|
||||
if (channel) return channel.show_id;
|
||||
const row = database
|
||||
.prepare(
|
||||
`SELECT show_id FROM presentation_access_tokens
|
||||
@@ -508,15 +600,30 @@ function presentationShowId(database: Database.Database, token: string) {
|
||||
return row?.show_id ?? null;
|
||||
}
|
||||
|
||||
export function getPresentationChannelView(channelId: string): PresentationView | null {
|
||||
const channel = getDatabase()
|
||||
.prepare('SELECT show_id, revision FROM presentation_channels WHERE id = ?')
|
||||
.get(channelId) as { show_id: string | null; revision: number } | undefined;
|
||||
if (!channel) return null;
|
||||
if (!channel.show_id) return { revision: channel.revision * 1_000_000_000, content: null };
|
||||
const view = getPresentationViewForShow(channel.show_id);
|
||||
return { ...view, revision: channel.revision * 1_000_000_000 + view.revision };
|
||||
}
|
||||
|
||||
export function getPresentationView(token: string): PresentationView | null {
|
||||
const database = getDatabase();
|
||||
const showId = presentationShowId(database, token);
|
||||
if (!showId) return null;
|
||||
return getPresentationViewForShow(showId);
|
||||
}
|
||||
|
||||
function getPresentationViewForShow(showId: string): PresentationView {
|
||||
const database = getDatabase();
|
||||
const state = ensureLiveState(database, showId, nowIso());
|
||||
if (!state.presentation_usage_id) return { revision: state.presentation_revision, content: null };
|
||||
const usage = database
|
||||
.prepare(
|
||||
`SELECT u.title_snapshot, u.presentation_snapshot, u.original_piece_id,
|
||||
`SELECT u.title_snapshot, u.output_title_snapshot, u.presentation_snapshot, u.original_piece_id,
|
||||
u.presentation_mode_snapshot, u.presentation_source_label_snapshot,
|
||||
u.presentation_source_url_snapshot, u.presentation_source_timecode_snapshot,
|
||||
u.presentation_title_meta_snapshot, u.presentation_description_snapshot,
|
||||
@@ -528,6 +635,7 @@ export function getPresentationView(token: string): PresentationView | null {
|
||||
.get(state.presentation_usage_id, showId) as
|
||||
| {
|
||||
title_snapshot: string;
|
||||
output_title_snapshot: string;
|
||||
presentation_snapshot: string;
|
||||
original_piece_id: string | null;
|
||||
presentation_mode_snapshot: PresentationContent['mode'];
|
||||
@@ -543,33 +651,11 @@ export function getPresentationView(token: string): PresentationView | null {
|
||||
}
|
||||
| undefined;
|
||||
if (!usage) return { revision: state.presentation_revision, content: null };
|
||||
const sources =
|
||||
usage.presentation_mode_snapshot === 'text' && usage.original_piece_id
|
||||
? (database
|
||||
.prepare(
|
||||
`SELECT label, url, timecode_seconds AS timecodeSeconds
|
||||
FROM piece_sources WHERE piece_id = ? ORDER BY position`
|
||||
)
|
||||
.all(usage.original_piece_id) as {
|
||||
label: string;
|
||||
url: string;
|
||||
timecodeSeconds: number | null;
|
||||
}[])
|
||||
: [];
|
||||
const images =
|
||||
usage.presentation_mode_snapshot === 'text' && usage.original_piece_id
|
||||
? (database
|
||||
.prepare(
|
||||
`SELECT id, alt_text AS altText FROM piece_media
|
||||
WHERE piece_id = ? ORDER BY position`
|
||||
)
|
||||
.all(usage.original_piece_id) as { id: string; altText: string }[])
|
||||
: [];
|
||||
return {
|
||||
revision: state.presentation_revision,
|
||||
content: {
|
||||
mode: usage.presentation_mode_snapshot,
|
||||
title: usage.title_snapshot,
|
||||
title: usage.output_title_snapshot || usage.title_snapshot,
|
||||
html: renderMarkdown(usage.presentation_snapshot),
|
||||
source: usage.presentation_source_url_snapshot
|
||||
? {
|
||||
@@ -589,8 +675,8 @@ export function getPresentationView(token: string): PresentationView | null {
|
||||
usage.presentation_provider_id_snapshot,
|
||||
usage.presentation_source_timecode_snapshot
|
||||
),
|
||||
sources,
|
||||
images
|
||||
sources: [],
|
||||
images: []
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -605,19 +691,11 @@ export async function getPresentationPreviewImage(token: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPresentationMedia(token: string, mediaId: string) {
|
||||
const database = getDatabase();
|
||||
const showId = presentationShowId(database, token);
|
||||
if (!showId) return null;
|
||||
const allowed = database
|
||||
.prepare(
|
||||
`SELECT 1 FROM show_live_states l
|
||||
JOIN piece_usages u ON u.id = l.presentation_usage_id
|
||||
JOIN show_sections s ON s.id = u.show_section_id
|
||||
JOIN piece_media m ON m.piece_id = u.original_piece_id
|
||||
WHERE l.show_id = ? AND s.show_id = ? AND m.id = ?`
|
||||
)
|
||||
.get(showId, showId, mediaId);
|
||||
if (!allowed) return null;
|
||||
return getMediaFile(mediaId);
|
||||
export async function getPresentationMedia(
|
||||
token: string,
|
||||
mediaId: string
|
||||
): Promise<{ data: Buffer; mimeType: string; byteSize: number; altText: string } | null> {
|
||||
void token;
|
||||
void mediaId;
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user