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:
parent
dfabc13e8e
commit
3452cd143a
13
src/db/migrations/20210311061107_create_audio_table.ts
Normal file
13
src/db/migrations/20210311061107_create_audio_table.ts
Normal 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");
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
22
src/types.ts
22
src/types.ts
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user