import * as path from 'path'; import * as request from 'requestretry'; import * as timestamp from 'unix-timestamp'; import * as jwt from 'jsonwebtoken'; import * as Sentry from '@sentry/node'; import { Response } from 'request'; // @ownzones import * as iam from '@ownzones/iam-service'; import { common } from '@ownzones/lib'; import { FileLocator, S3FileLocator } from '@ownzones/locators'; // app import { config, log } from '../config'; import { Honeycomb, TracedHttpAgent, TracedHttpsAgent } from './tracing'; import { Nullable } from '../utils/types'; type GqlQuery = { query: string, variables: object }; interface GqlResponse extends Response { body: { data?: TData errors?: Error[] } } // ToDo we can export most of these from connect export interface IUser { id: string; email: string; organization: IOrganization; } export interface IOrganization { id: string; slug: string; // ToDo: this field appear in iam-service as commented out, does not exist on org maxCacheSize: number; // ToDo: this field appear in iam-service as commented out, does not exist on org cacheLocation: string; orgId?: string; organizationOptions?: IOrganizationOptions mediaViewCacheFileLocator: FileLocator; } export interface IOrganizationOptions { cacheCapacity: string, } export enum CompositionSequenceType { Image = 'image', Audio = 'audio', TimedText = 'timed_text', Marker = 'marker', } export enum TrackType { Audio = 'audio', Data = 'data', Video = 'video', TimedText = 'timed_text', DVMetadata = 'dv_metadata', } export interface IFile { id: string, locatorUrl?: string, fileLocator?: S3FileLocator, mxfIndexLocator?: FileLocator, title: ITitle, } export interface ITitle { id: string, name: string, } export interface ILocalizedValue { language?: string; value: string; } export interface ICompositionVersion { id: string; labelText: ILocalizedValue; } export interface IEssenceDescriptor { id: string; labelText: ILocalizedValue; } export interface ICompositionMarker { annotation?: string; label: string; offset: number | string; } export interface IImfTrackFileResource { sourceEncoding?: string; trackId?: string; keyId?: string; hash?: string; HashAlgorithm?: string; } export interface IImfBaseResource { id?: string; annotation?: string; editRate?: IEditRate; intrinsicDuration?: number; entryPoint: number; // ToDo Resolve in connect, but this is technically always a number, can be NaN, but always a number. sourceDuration: number; // same as above repeatCount?: number; } export interface IImfMarkerResource { markers?: ICompositionMarker[]; } export interface IResource extends IImfTrackFileResource, IImfBaseResource, IImfMarkerResource { editUnit?: number; trackType?: TrackType; isFlat?: boolean; fileId?: string; trackFileLocator: FileLocator; track: ITrack; essenceDescriptor?: object; essenceId?: string; sourceDurationSec?: number; fileProperties?: any; } export interface ITrack { fileId: string; file?: IFile; type: TrackType; properties: ITrackProperties; } export interface ITrackProperties { codec: string; codecLongName: string; probeResult: IFFProbeStream; } export interface IFFProbeStream { index?: number; width: number; height: number; fieldHeight?: number; pixFmt: string; codecName: string; startTime: string; bitsPerRawSample: string; channels: number; channelLayout: string; duration: string; sampleRate?: string; } export interface ISequence { id?: string; type: CompositionSequenceType; firstFrameIndex: Nullable; virtualTrackId: string; resources: IResource[]; } export interface ICompositionSegment { id?: string; sequences: ISequence[]; annotation?: string; } export interface ICompositionTimecode { timecodeDropFrame: boolean; timecodeRate: number; timecodeStartAddress: string; } export interface IContentKind { scope?: string; value: string; language?: string; } export interface ICompositionContentMaturity { agency?: string; rating?: string; audience?: { scope?: string; value?: string; }; } export interface ICompositionLocale { annotation?: string; region?: string[]; contentMaturity?: ICompositionContentMaturity[]; language?: string[]; } export interface IEditRate { numerator: number; denominator: number; } export interface ICompositionPlaylist { id: string; name?: string; markers?: ICompositionMarker[]; annotation?: ILocalizedValue; issueDate: Date; issuer?: ILocalizedValue; creator?: ILocalizedValue; contentOriginator?: ILocalizedValue; contentTitle?: ILocalizedValue; contentKind?: IContentKind; contentVersions?: ICompositionVersion[]; essenceDescriptorList?: IEssenceDescriptor[]; compositionTimecode?: ICompositionTimecode; editRate?: IEditRate; totalRunningTime?: string; localeList?: ICompositionLocale[]; extensionProperties?: string[]; segments: ICompositionSegment[]; } export interface IComposition extends ICompositionPlaylist { audioSampleRate?: IEditRate; editUnit?: number; file: IFile; virtualTracks?: any[]; fileId?: string; dpp?: boolean; maxCLL?: number; maxFALL?: number; applicationType?: string; } export interface ITask { id: string; workflowId: string; } export interface IJob { fileId: string; compositionDefinition: IComposition; organization: IOrganization; user?: IUser; cacheAllAudio?: boolean; extractChannels?: boolean; task?: ITask; fileIsCpl?: boolean; } function delayStrategy(): number { return Math.floor(Math.random() * (3000 - 500 + 1) + 500); } async function makeGQLQuery( payload: GqlQuery, accessToken: string, orgSlug: string, gqlEndpoint: string = config.graphQLEndpoint, ): Promise { const makeGqlQuerySpan = Honeycomb.startSpan(path.basename(__filename), 'makeGqlQuery'); const response = await request.post({ delayStrategy, maxAttempts: 3, retryStrategy: request.RetryStrategies.HTTPOrNetworkError, url: gqlEndpoint, agent: !config.sslEndpoints ? new TracedHttpAgent() : new TracedHttpsAgent(), headers: { 'X-Organization-Slug': orgSlug, Authorization: accessToken, }, json: true, body: payload, }) as GqlResponse; if (!response.body.data || response.body?.errors?.length) { Sentry.captureException(response); throw new Error(`Request failed with ${JSON.stringify(response)}`); } Honeycomb.endSpan(makeGqlQuerySpan); return response.body.data; } function getAuthorizationHeader(ctx: iam.IGraphQlCtx): string { return `Bearer ${getJwtToken(ctx)}`; } // ToDo ctx.userId is undefined... Maybe remove this function entirely since we can use the ctx.user.authorization function getJwtToken(ctx: iam.IGraphQlCtx): string { const payload = { exp: timestamp.now('2m'), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access userId: (ctx as any).user?.id, system: true, organizationId: ctx.organizationId, organizationSlug: ctx.organizationSlug, }; return jwt.sign(payload, config.jwtSecret); } export async function getCompositionDefinition(fileId: string, orgSlug: string, trackIds: string[] = []): Promise<{ compositionDefinition: IComposition, org: Pick & { orgId?: string } }> { const payload = { query: `query CompositionDefinition($fileId: ID!, $trackIds: [String!]) { compositionDefinition(fileId: $fileId, trackIds: $trackIds) { id file { id locatorUrl title { id name } } segments { sequences { type virtualTrackId firstFrameIndex resources { id track { file { id locatorUrl mxfIndexLocator { url } } fileId type properties { codec codecLongName probeResult } } trackId trackFileLocator { url } editRate { numerator denominator } intrinsicDuration entryPoint sourceDuration repeatCount sourceEncoding } } } } }`, variables: { fileId, trackIds }, }; // ToDo Move to separate method const organization = await Honeycomb.startAsyncSpan( path.basename(__filename), 'getOrganization', async (span) => { // ToDo Expose the types in iam-service lib const resp = await iam.getOrganizationBySlug(orgSlug, iam.setContext()) as IOrganization | null; Honeycomb.endSpan(span); return resp; }, { orgSlug }, ); if (!organization) { throw new Error(`Organization with slug ${orgSlug} not found!`); } const internalAccessToken = getAuthorizationHeader(iam.setContext(undefined, organization.id, orgSlug)); const getCompDefSpan = Honeycomb.startSpan(path.basename(__filename), 'getCompositionDef'); const response = await makeGQLQuery<{ compositionDefinition: IComposition }>(payload, internalAccessToken, orgSlug); const { compositionDefinition } = response; const cacheLocation = common.ensureTrailingSlash(organization.mediaViewCacheFileLocator.url); let maxCacheSize = organization.organizationOptions?.cacheCapacity != null ? Number.parseInt(organization.organizationOptions.cacheCapacity, 10) : config.maxCache; if (maxCacheSize == null || Number.isNaN(maxCacheSize) || maxCacheSize < 0) { log.warn('Maximum cache got from database is in an invalid format'); maxCacheSize = config.maxCache; } const org: Pick & { orgId?: string } = { maxCacheSize, cacheLocation, orgId: organization.slug, // ToDo ? mediaViewCacheFileLocator: organization.mediaViewCacheFileLocator, }; Honeycomb.endSpan(getCompDefSpan); return { compositionDefinition, org }; } export async function signalMediaviewPrecacheTask(taskId: string, workflowId: string, user: IUser, orgSlug: string): Promise { const payload = { query: `mutation SignalMediaviewPrecacheTask($taskId: ID!, $workflowId: ID!) { signalMediaviewPrecacheTask(taskId: $taskId, workflowId: $workflowId) }`, variables: { taskId, workflowId }, }; const organization = await Honeycomb.startAsyncSpan(path.basename(__filename), 'getOrganization', async (span) => { const resp = await iam.getOrganizationBySlug(orgSlug, iam.setContext()) as IOrganization | null; Honeycomb.endSpan(span); return resp; }, { orgSlug }); if (!organization) { throw new Error(`Organization with slug ${orgSlug} not found!`); } const internalAccessToken = getAuthorizationHeader(iam.setContext(user, organization.id, orgSlug)); const signalMediaviewPrecacheTaskSpan = Honeycomb.startSpan(path.basename(__filename), 'signalMediaviewPrecacheTask'); const response = (await makeGQLQuery(payload, internalAccessToken, orgSlug)); Honeycomb.endSpan(signalMediaviewPrecacheTaskSpan); return response; } export async function getFile(fileId: string, orgSlug: string): Promise { const payload = { query: `query File($id: ID!) { file(id: $id) { id fileLocator { type ... on S3FileLocator { bucket key secretAccessKey accessKeyId etag url } } } }`, variables: { id: fileId }, }; const organization = await Honeycomb.startAsyncSpan(path.basename(__filename), 'getOrganization', async (span) => { const resp = await iam.getOrganizationBySlug(orgSlug, iam.setContext()) as IOrganization | null; Honeycomb.endSpan(span); return resp; }, { orgSlug }); if (!organization) { throw new Error(`Organization with slug ${orgSlug} not found!`); } const internalAccessToken = getAuthorizationHeader(iam.setContext(undefined, organization.id, orgSlug)); const getFileSpan = Honeycomb.startSpan(path.basename(__filename), 'getFile'); const response = await makeGQLQuery<{file: IFile}>(payload, internalAccessToken, orgSlug); Honeycomb.endSpan(getFileSpan); return response.file; }