mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(server): type the email module and fix narration audio serving
Three defects, found while planning the Android client: - The email module's providers, preferences, templates, recipient groups, tests and send requests had no types in packages/types, so every client mirrored them from server code. They are typed now, from the JSON the routes return and accept, and the services and DTOs declare them. No response changes. - Narration audio paths were decoded twice: the router decodes the wildcard, and the route decoded it again, so a name holding a literal % resolved to the wrong entry or none. A path is now decoded once and matched to the playlist's hrefs exactly, with an exact entry name preferred. Each request also read the whole audio entry into memory; it is now streamed, read from its offset when stored and inflated and skipped when deflated, with the same headers and Range handling, and an open range at or past the end is a 416. Both narration audio routes skip response compression, which dropped Content-Length. - A playlist's section starts left out any clip of unknown length. They now use the media:duration each overlay declares when a clip's own length is unknown. A seq that interleaves par and seq children was reordered by the XML parser's grouping; SMIL is now read in document order.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { BookSelectionPayload } from "./book-selection";
|
||||
import type { DeliveryArtifactMetadata, ReadAloudDeliveryPreference } from "./delivery";
|
||||
|
||||
export interface EmailRecipient {
|
||||
@@ -76,3 +77,194 @@ export interface EmailSendLogSummary {
|
||||
failed: number;
|
||||
pending: number;
|
||||
}
|
||||
|
||||
/** Every provider route answers with the stored row minus its password. */
|
||||
export interface EmailProvider {
|
||||
id: number;
|
||||
/** Null for a provider no account owns. */
|
||||
userId: number | null;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string | null;
|
||||
fromName: string | null;
|
||||
fromAddress: string | null;
|
||||
auth: boolean;
|
||||
ssl: boolean;
|
||||
startTls: boolean;
|
||||
isDefault: boolean;
|
||||
isShared: boolean;
|
||||
isSystemProvider: boolean;
|
||||
tlsRejectUnauthorized: boolean;
|
||||
/** Set by the last connection test; absent from servers before 3.4.0. */
|
||||
lastTestedAt?: string | null;
|
||||
lastTestOk?: boolean | null;
|
||||
lastTestError?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
/** POST /email/providers */
|
||||
export interface EmailProviderCreateRequest {
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
fromName?: string;
|
||||
fromAddress?: string;
|
||||
auth: boolean;
|
||||
ssl: boolean;
|
||||
startTls: boolean;
|
||||
tlsRejectUnauthorized: boolean;
|
||||
}
|
||||
|
||||
/** PUT /email/providers/:id. An empty password removes the stored one. */
|
||||
export type EmailProviderUpdateRequest = Partial<EmailProviderCreateRequest>;
|
||||
|
||||
/** POST /email/providers/:id/test and POST /email/recipients/:id/test. */
|
||||
export interface EmailTestResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET and PUT /email/preferences: the defaults sending uses, including any still carried by a
|
||||
* row's legacy isDefault flag. An account that never saved preferences gets only userId and the
|
||||
* three defaults.
|
||||
*/
|
||||
export interface EmailPreferences {
|
||||
id?: number;
|
||||
userId: number;
|
||||
defaultProviderId: number | null;
|
||||
defaultRecipientId: number | null;
|
||||
defaultTemplateId: number | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** PUT /email/preferences. An omitted default is left alone; null clears it. */
|
||||
export interface EmailPreferencesUpdateRequest {
|
||||
defaultProviderId?: number | null;
|
||||
defaultRecipientId?: number | null;
|
||||
defaultTemplateId?: number | null;
|
||||
}
|
||||
|
||||
export interface EmailTemplate {
|
||||
id: number;
|
||||
/** Null for a built-in template. */
|
||||
userId: number | null;
|
||||
name: string;
|
||||
subject: string;
|
||||
bodyText: string;
|
||||
isDefault: boolean;
|
||||
isSystem: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** POST /email/templates */
|
||||
export interface EmailTemplateCreateRequest {
|
||||
name: string;
|
||||
subject: string;
|
||||
bodyText: string;
|
||||
}
|
||||
|
||||
/** PUT /email/templates/:id */
|
||||
export type EmailTemplateUpdateRequest = Partial<EmailTemplateCreateRequest>;
|
||||
|
||||
/** POST /email/templates/:id/preview */
|
||||
export interface EmailTemplatePreviewRequest {
|
||||
bookId: number;
|
||||
}
|
||||
|
||||
/** POST /email/templates/preview: text from the editor, before it is saved. */
|
||||
export interface EmailTemplateTextPreviewRequest {
|
||||
subject: string;
|
||||
bodyText: string;
|
||||
bookId: number;
|
||||
}
|
||||
|
||||
/** POST /email/templates/:id/preview */
|
||||
export interface EmailTemplatePreview {
|
||||
subject: string;
|
||||
bodyText: string;
|
||||
}
|
||||
|
||||
/** The file a send of the previewed book would attach, named as the email names it. */
|
||||
export interface EmailTemplatePreviewAttachment {
|
||||
fileName: string;
|
||||
format: string | null;
|
||||
sizeBytes: number | null;
|
||||
}
|
||||
|
||||
/** POST /email/templates/preview */
|
||||
export interface EmailTemplateTextPreview extends EmailTemplatePreview {
|
||||
/** Null when the book has no file a send could attach; absent from servers before 3.4.0. */
|
||||
attachment?: EmailTemplatePreviewAttachment | null;
|
||||
}
|
||||
|
||||
/** PUT /email/recipient-groups/:id answers this, without the members. */
|
||||
export interface EmailRecipientGroupSummary {
|
||||
id: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
defaultTemplateId: number | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Every other recipient group route that answers with a group. */
|
||||
export interface EmailRecipientGroup extends EmailRecipientGroupSummary {
|
||||
members: EmailRecipient[];
|
||||
}
|
||||
|
||||
/** POST /email/recipient-groups */
|
||||
export interface EmailRecipientGroupCreateRequest {
|
||||
name: string;
|
||||
defaultTemplateId?: number;
|
||||
}
|
||||
|
||||
/** PUT /email/recipient-groups/:id */
|
||||
export interface EmailRecipientGroupUpdateRequest {
|
||||
name?: string;
|
||||
defaultTemplateId?: number | null;
|
||||
}
|
||||
|
||||
/** POST /email/recipient-groups/:id/members */
|
||||
export interface EmailRecipientGroupMemberRequest {
|
||||
recipientId: number;
|
||||
}
|
||||
|
||||
/** POST /email/recipients/:id/test, written in the sender's interface language. */
|
||||
export interface EmailRecipientTestRequest {
|
||||
subject: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** Who and how to send, beside the book selection of POST /email/send. */
|
||||
export interface EmailSendOptions {
|
||||
recipientIds?: number[];
|
||||
groupIds?: number[];
|
||||
fileId?: number;
|
||||
providerId?: number;
|
||||
templateId?: number;
|
||||
}
|
||||
|
||||
/** POST /email/send. It needs at least one recipient, given directly or through a group. */
|
||||
export type EmailSendRequest = BookSelectionPayload & EmailSendOptions;
|
||||
|
||||
/** POST /email/send, POST /email/send/quick/:bookId and POST /email/log/:id/resend. */
|
||||
export interface EmailSendAccepted {
|
||||
queued: number;
|
||||
}
|
||||
|
||||
/** The query of GET /email/log; GET /email/admin/log reads only page and size. */
|
||||
export interface EmailSendLogQuery {
|
||||
page?: number;
|
||||
size?: number;
|
||||
bookId?: number;
|
||||
status?: EmailSendLogEntry["status"];
|
||||
recipientId?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user