Add "recordings" table, save raw audio blobs in it

Other changes:
- Move Stored(Voicemail|Recording) interfaces into knex/types/tables
module for reduced boilerplate.
- Change updateStoredVoicemail to take Partial<StoredVoicemail>,
allowing to only update only some columns
This commit is contained in:
Matt Low 2021-03-11 10:59:08 -07:00
parent dfabc13e8e
commit 3452cd143a
4 changed files with 89 additions and 48 deletions

View File

@ -0,0 +1,13 @@
import { Knex } from "knex";
export async function up(knex: Knex) {
await knex.schema.createTable("recordings", (table) => {
table.bigInteger("messageId").primary().references("voicemails.messageId");
table.string("mimeType", 32);
table.binary("audio");
});
}
export async function down(knex: Knex) {
await knex.schema.dropTable("recordings");
}

View File

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
import type { Contact, StoredVoicemail } from "./types";
import { getNationalNumber, formatSeconds } from "./util"; import { getNationalNumber, formatSeconds } from "./util";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import type { Contact } from "./types";
import type { StoredVoicemail } from "knex/types/tables";
export function getTicketSubject( export function getTicketSubject(
voicemail: StoredVoicemail, voicemail: StoredVoicemail,

View File

@ -11,8 +11,8 @@ import type {
RCAudioAttachment, RCAudioAttachment,
Recording, Recording,
Transcription, Transcription,
StoredVoicemail,
} from "./types"; } from "./types";
import type { StoredVoicemail, StoredRecording } from "knex/types/tables";
const SEARCH_CONTACT_BY_PHONE_NUMBER_QUERY = gql` const SEARCH_CONTACT_BY_PHONE_NUMBER_QUERY = gql`
query getContactByPhoneNumber($phoneNumber: String!) { query getContactByPhoneNumber($phoneNumber: String!) {
@ -168,8 +168,8 @@ export function ticketize(
const response = await rcsdk.get(audio.uri); const response = await rcsdk.get(audio.uri);
const result = { const result = {
duration: audio.vmDuration, duration: audio.vmDuration,
mimetype: audio.contentType, mimeType: audio.contentType,
audio: await response.blob(), audio: await response.arrayBuffer(),
}; };
return result; return result;
} }
@ -193,7 +193,10 @@ export function ticketize(
* @param voicemail * @param voicemail
* @param contact * @param contact
*/ */
async function createTicket(voicemail: StoredVoicemail, contact?: Contact) { async function createTicket(
voicemail: StoredVoicemail & StoredRecording,
contact?: Contact
) {
const input: any = { const input: any = {
subject: getTicketSubject(voicemail, contact), subject: getTicketSubject(voicemail, contact),
description: getTicketBody(voicemail, contact), description: getTicketBody(voicemail, contact),
@ -229,7 +232,8 @@ export function ticketize(
recording: Recording, recording: Recording,
transcription: Transcription transcription: Transcription
) { ) {
return db<StoredVoicemail>("voicemails").insert({ await db.transaction(async (trx) => {
await trx("voicemails").insert({
messageId: message.id, messageId: message.id,
extensionId: message.extensionId, extensionId: message.extensionId,
received: message.creationTime, received: message.creationTime,
@ -242,16 +246,24 @@ export function ticketize(
transcriptionStatus: transcription.status, transcriptionStatus: transcription.status,
transcription: transcription.text, transcription: transcription.text,
}); });
await trx("recordings").insert({
messageId: message.id,
mimeType: recording.mimeType,
audio: new Uint8Array(recording.audio),
});
});
} }
/** /**
* Updates a stored voicemail using its current properties * Updates a stored voicemail
* @param voicemail the voicemail to update * @param voicemail the voicemail to update
*/ */
async function updateStoredVoicemail(voicemail: StoredVoicemail) { async function updateStoredVoicemail(voicemail: Partial<StoredVoicemail>) {
await db<StoredVoicemail>("voicemails") const messageId = voicemail.messageId;
.update({ ...voicemail }) if (!messageId) {
.where({ messageId: voicemail.messageId }); throw new Error("Missing required messageId property");
}
await db("voicemails").update(voicemail).where({ messageId });
} }
/** /**
@ -259,9 +271,7 @@ export function ticketize(
* @returns whether the message by the given ID has been stored * @returns whether the message by the given ID has been stored
*/ */
async function isMessageStored(messageId: number) { async function isMessageStored(messageId: number) {
const result = await db<StoredVoicemail>("voicemails") const result = await db("voicemails").where({ messageId }).first();
.where({ messageId })
.first();
return result !== undefined; return result !== undefined;
} }
@ -269,7 +279,8 @@ export function ticketize(
* @returns stored voicemails that haven't had tickets created for them yet * @returns stored voicemails that haven't had tickets created for them yet
*/ */
async function getUnprocessedVoicemails() { async function getUnprocessedVoicemails() {
return await db<StoredVoicemail>("voicemails") return await db("voicemails")
.join("recordings", "voicemails.messageId", "recordings.messageId")
.whereNull("ticketId") .whereNull("ticketId")
.whereIn("transcriptionStatus", [ .whereIn("transcriptionStatus", [
"Completed", "Completed",
@ -283,7 +294,7 @@ export function ticketize(
* @returns stored voicemails whose trranscriptions may still be in progress * @returns stored voicemails whose trranscriptions may still be in progress
*/ */
async function getMissingTranscriptionVoicemails() { async function getMissingTranscriptionVoicemails() {
return await db<StoredVoicemail>("voicemails") return await db("voicemails")
.whereNotNull("transcription") .whereNotNull("transcription")
.whereNotIn("transcriptionStatus", [ .whereNotIn("transcriptionStatus", [
// Don't include those whose transcriptions have failed or will not // Don't include those whose transcriptions have failed or will not
@ -366,7 +377,11 @@ export function ticketize(
// else we do nothing // else we do nothing
return; return;
} }
return updateStoredVoicemail(message); return updateStoredVoicemail({
messageId: message.messageId,
transcriptionStatus: message.transcriptionStatus,
transcription: message.transcription,
});
}) })
); );
} }
@ -385,7 +400,7 @@ export function ticketize(
`Created ticket ${ticketId} from voicemail ${voicemail.messageId}` `Created ticket ${ticketId} from voicemail ${voicemail.messageId}`
); );
return updateStoredVoicemail({ return updateStoredVoicemail({
...voicemail, messageId: voicemail.messageId,
ticketId, ticketId,
contactId: contact?.id, contactId: contact?.id,
contactableType: contact?.contactable.__typename, contactableType: contact?.contactable.__typename,

View File

@ -97,8 +97,8 @@ export interface RCMessage {
export interface Recording { export interface Recording {
duration: number; duration: number;
mimetype: string; mimeType: string;
audio: Blob; audio: ArrayBuffer;
} }
export interface Transcription { export interface Transcription {
@ -106,16 +106,16 @@ export interface Transcription {
text: string | null; text: string | null;
} }
export interface StoredVoicemail { declare module "knex/types/tables" {
interface StoredVoicemail {
messageId: number; messageId: number;
extensionId: number; extensionId: number;
processed: boolean;
received: string; received: string;
toNumber: string; toNumber: string;
extensionNumber: string; extensionNumber: string;
extensionName: string; extensionName: string;
fromNumber: string; fromNumber: string;
fromName?: string; fromName: string;
duration: number; duration: number;
transcriptionStatus: TranscriptionStatus; transcriptionStatus: TranscriptionStatus;
transcription: string | null; transcription: string | null;
@ -124,3 +124,15 @@ export interface StoredVoicemail {
contactableType?: string; contactableType?: string;
contactableId?: number; contactableId?: number;
} }
interface StoredRecording {
messageId: number;
mimeType: string;
audio: ArrayBuffer;
}
interface Tables {
voicemails: StoredVoicemail;
recordings: StoredRecording;
}
}