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 ReactDOMServer from "react-dom/server";
import type { Contact, StoredVoicemail } from "./types";
import { getNationalNumber, formatSeconds } from "./util";
import { DateTime } from "luxon";
import type { Contact } from "./types";
import type { StoredVoicemail } from "knex/types/tables";
export function getTicketSubject(
voicemail: StoredVoicemail,

View File

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

View File

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