<template>
- <div id="app" v-if="ready">
+ <div id="app" v-if="(windowReady && sessionReady)">
<button class="titlebar" />
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
+import { IpcRendererEvent } from "electron";
+import { authRequest } from '@/modules/message';
import { useIpc } from "@/modules/ipc";
-import Splash from "@/components/splash.vue"
+
+import Splash from "@/components/splash.vue";
+import Messenger from "@/components/messenger.vue";
+import Login from "@/components/login.vue";
export default defineComponent({
- components: { Splash },
+ components: {
+ Splash,
+ Messenger,
+ Login
+ },
setup() {
+ const { post, invoke } = useIpc();
+
+ const auth = ref(false);
+ const windowReady = ref(false);
+ const sessionReady = ref(false);
+
+ // When connection is established, we send key over.
+ const onOpen = (_event: IpcRendererEvent, payload: any) => {
+ console.log(`Connected to Crimata.`)
+ const key = window.localStorage.getItem("key");
- const ready = ref(false);
+ if (key) {
+ console.log(`Sending key: ${key}`)
+ post("client-message", authRequest(key, false, false))
+ }
- // Whether user is logged in.
- let auth = ref(false);
+ else {
+ console.log("No key, session ready.")
+ sessionReady.value = true
+ }
- // Attempt token login
- const key = window.localStorage.getItem(AUTH_KEY);
-
- if (key) {
- post("client-message", key)
}
- // onAuthResponse
- const onAuthResponse = (_event: IpcMainEvent, payload: Auth) => {
- if (payload.token) {
+ // Update authenticate state on auth message from server.
+ const onAuthResponse = (_event: IpcRendererEvent, payload: any) => {
+ const key = payload.message.key
+ const usr = payload.message.usr
+ console.log(`Received auth response: ${usr}, ${key}`)
+
+ if (key) {
+ console.log(`Auth success, saving key: ${key}.`)
+ window.localStorage.setItem("key", payload.message.key)
auth.value = true;
}
+
+ else {
+ console.log(`Auth failed, clearing local storage.`)
+ window.localStorage.clear()
+ auth.value = false;
+ }
+
+ console.log("Session is ready.")
+ sessionReady.value = true
+
}
// Post navbar action to backend.
const callNavbar = (action: string) => {
- post('nav-bar', action);
+ invoke("nav-bar", action);
}
-
onMounted(() => {
- window.ipcRenderer.on("auth-response", onAuthResponse)
-
-
- window.ipcRenderer.on("window-ready", () => ready.value = true);
+ window.ipcRenderer.on("on-connect", onOpen)
+ window.ipcRenderer.on("auth-response", onAuthResponse)
+ window.ipcRenderer.on("window-ready", () => windowReady.value = true);
});
onUnmounted(() => {
- window.ipcRenderer.removeAllListeners("window-ready")
- window.ipcRenderer.removeAllListeners("auth-response")
+ window.ipcRenderer.removeAllListeners("on-connect")
+ window.ipcRenderer.removeAllListeners("window-ready")
+ window.ipcRenderer.removeAllListeners("auth-response")
})
return {
callNavbar,
- ready,
- auth,
+ sessionReady,
+ windowReady,
+ auth
}
}
})
input: '',
}
-const encoding = "hex";
-
const audioOptions = {
channelCount: 1,
sampleFormat: 16,
closeOnError: false,
}
-// callback run on space bar key up and down.
-const updateRecorder = (_event, payload: { isRecording: boolean; uid: string | null;
-
-}): void => {
-
- record = payload.isRecording;
- if (!record) {
- if (payload.uid) sendAudio(audioContainer.input, payload.uid);
- }
-};
+// Returns recorded audio to frontend and sets record to false.
+const onRecordingEnd = async (_event: any, payload: any) => {
-// listen for space key up/down event.
-ipcMain.removeAllListeners('update-recorder');
-ipcMain.on('update-recorder', updateRecorder);
+ return new Promise((resolve, reject) => {
-// Split Buffer into an array of len-sized Buffers.
-function bufSplit(buf: Buffer, len: number): Array<Buffer> {
- const chunks = [];
- let i = 0;
- let L = len;
+ try {
+ resolve(audioContainer.input);
+ record = false;
+ } catch (e) {
+ reject()
+ }
- while(i < buf.byteLength) {
- chunks.push(buf.slice(i, L));
- i = L;
- L += len;
- }
+ });
+};
- return chunks;
-}
// Main audio function run by run.ts module.
export function initAudioIO(): void {
+ console.log("AUDIO:Starting io streams.")
if (!ai) {
- ai = new portAudio.AudioIO({ inOptions: audioOptions });
- // base64 encoding needed for google speech to text.
- ai.setEncoding(encoding);
+ // Initialize and start input stream.
+ ai = new portAudio.AudioIO({ inOptions: audioOptions });
+ ai.setEncoding("hex");
ai.start();
+ // On each data chunk...
ai.on('data', (chunk: string) => {
+
+ // If recording, we capture the data.
if (record) {
- console.log('recording...')
+ console.log('Recording...')
audioContainer.input += chunk;
- } else {
- if (audioContainer.input.length) audioContainer.input = "";
}
+
+ // Else, we don't capture and also clear audioContainer.
+ else {
+ if (audioContainer.input.length) {
+ audioContainer.input = "";
+ }
+ }
+
});
}
if (!ao) {
+
+ // Initialize and start input stream.
ao = new portAudio.AudioIO({ outOptions: audioOptions });
ao.start();
+
}
+
+ // Listen to record.
+ console.log("AUDIO:Adding recording listeners.")
+
+ ipcMain.removeAllListeners('start-recording');
+ ipcMain.on('start-recording', () => record = true);
+
+ ipcMain.removeHandler('stop-recording');
+ ipcMain.handle('stop-recording', onRecordingEnd);
+}
+
+
+// ---Audio playback--------------------------------------------
+
+// Split Buffer into an array of len-sized Buffers.
+function bufSplit(buf: Buffer, len: number): Array<Buffer> {
+ const chunks = [];
+ let i = 0;
+ let L = len;
+
+ while(i < buf.byteLength) {
+ chunks.push(buf.slice(i, L));
+ i = L;
+ L += len;
+ }
+
+ return chunks;
}
// Audio playback.
}
}
+// -------------------------------------------------------------
+
// Get's called on window close.
export function stopStream() {
+ console.log("AUDIO:Stopping audio stream.")
if(ai != null) {
ai.quit();
ai = null;
}
}
+
+// // Returns recorded audio to frontend.
+// const getAudio = async (_event: any, payload: any) => {
+
+// return new Promise((resolve, reject) => {
+
+// // Handle auth response from server.
+// backgroundMitt.once('auth-res', (res: string) => {
+
+// if (res == "locked") {
+// reject(res);
+// } else {
+// console.log('Success!');
+// auth = true;
+// resolve(res);
+// }
+
+// });
+
+// if (typeof payload !== "string") {
+// payload = JSON.stringify(payload)
+// }
+
+// // Send token to backend
+// socket.send(payload);
+
+// });
+// };
+
"use strict";
import { app } from "electron";
-import { main } from './run';
-import { backgroundMitt } from './emitter';
-import { stopStream } from './audio';
+import { createWindow } from './window';
+import { initSession } from './session';
+import { initAudioIO, stopStream } from './audio';
+import { backgroundMitt } from '@/modules/emitter';
-let winActive: boolean;
+let win: boolean;
+// Listen for window creation.
backgroundMitt.on('window-active', (state: boolean) => {
- winActive = state;
+ win = state;
});
+// Run when electron app is initialized.
+async function main() {
+ console.log("MAIN:Initializing Electron App.")
+
+ // Must wait til window is created.
+ await createWindow();
+
+ // Instantiate socket session with crimata-platorm.
+ initSession();
+
+ // Begin audio stream.
+ initAudioIO();
+
+}
+
+// Root function of app.
export function initApp(dev: boolean): void {
+ // On initial startup.
app.on("ready", () => {
- main();
+ main()
});
- // Quit when all windows are closed.
+ // Quit app on window closed.
app.on("window-all-closed", () => {
- if (process.platform !== "darwin") {
- stopStream();
- app.quit();
- }
+ console.log("MAIN:Quitting app.")
+ app.quit()
+ });
+
+ // Shutdown audio streams peacefully.
+ app.on("before-quit", () => {
+ stopStream()
});
+ // When user clicks app icon (re-open)
app.on("activate", () => {
- if (winActive === false) {
- main();
+
+ if (!win) {
+ createWindow();
}
+
});
// Exit cleanly on request from parent process in development mode.
+++ /dev/null
-"use strict";
-
-import { createWindow } from './window';
-import agentInterface from './session';
-import { initAudioIO } from './audio';
-
-/*
- * The main function will be run after electron app is ready.
- */
-export async function main() {
-
- const { initSession } = agentInterface()
-
- // create main window.
- await createWindow();
-
- // Instantiate socket session with crimata-platorm.
- initSession();
-
- // Begin audio stream.
- initAudioIO();
-
-}
+/*
+ * Creates a websocket session with Crimata Servers.
+ *
+ * Connects to Servers and attempts token authentication. Will send the result
+ * of the authentication to the window. It will then serve as a communication
+ * interface between the window and the servers. It will automatically try to
+ * reconnect on websocket disconnect.
+ */
+
import { backgroundMitt } from './emitter';
import { ipcMain, IpcMainEvent } from "electron";
import { handleAuthMessage, handleStandardMessage, handleProfileMessage, handleAnnotation } from "./handlers";
+// Key for key-based auth.
+let key: string;
+
+
+
// Calls appropriate endpoint for a server message.
-const onServerMessage = (data: string): void => {
+const onMessage = (data: string) => {
const message = JSON.parse(data)
- if (message.key) {
- backgroundMitt.emit('auth-response', message);
+ if (message.hasOwnProperty("key")) {
+ handleAuthMessage(message)
}
else if (message.content) {
};
-const { createSocket, sendMessage } = useWebSockets(onServerMessage);
-
-// Handle messages from client.
-const onClientMessage = (_event: IpcMainEvent, payload: any) {
- sendMessage(JSON.stringify(payload))
+// Attempt token authentication onOpen.
+const onOpen = () => {
+ if (key) {
+ sendMessage({
+ "key": key,
+ "usr": false,
+ "pwd": false
+ })
+ }
}
-// Routes messages to and from Crimata Servers.
-export default function agentInterface() {
-
- const initSession = () => {
+const { createSocket, sendMessage } = useWebSockets(onMessage, onOpen);
- ipcMain.removeAllListeners()
+// Handle messages from window/client.
+const onMessageFromWindow = (_event: IpcMainEvent, payload: any) => {
+ console.log("New client message")
- ipcMain.on("client-message", onClientMessage);
+ const success = sendMessage(payload)
- createSocket()
+ if (!success) {
+ console.log("Unable to send message: ", payload)
}
+
+}
- return {
- initSession
- }
+// Call this to initialize session with Crimata servers.
+export const initSession = (key: string) => {
+
+ // Set the key.
+ key = key;
+
+ // Open socket connection.
+ createSocket()
+
+ // Attach listeners for frontend.
+ ipcMain.removeAllListeners("client-message")
+ ipcMain.on("client-message", onMessageFromWindow);
+
}
\ No newline at end of file
// Called when a NavBar button is pressed.
const onNavBar = (_event: any, action: string): void => {
- console.log("onNavBar")
if (win) {
if (action === "close") {
win.close()
// Do this on window mount.
const onWindowMount = (): void => {
+ console.log("BW:Adding window listeners. ")
+
+ // Must tell initApp that window exists.
backgroundMitt.emit('window-active', true);
// Handle win nav-bar event.
- ipcMain.removeHandler('nav-bar'); // avoid setting duplicate handlers
- ipcMain.handle('nav-bar', onNavBar);
+ ipcMain.removeHandler("nav-bar") // avoid setting duplicate handlers
+ ipcMain.handle("nav-bar", onNavBar);
// Gateway for messages to the frontend.
+ backgroundMitt.removeAllListeners('ipc-renderer')
backgroundMitt.on('ipc-renderer', renderMessage);
+
+ console.log("BW:Listeners created.")
}
// Do this on window dismount (close).
const onWindowDismount = (): void => {
+ console.log("BW:Window closed.")
win = null;
backgroundMitt.emit('window-active', false);
}
// function used by run.ts to create the main window.
export async function createWindow(): Promise<void> {
return new Promise((resolve, _reject) => {
+ console.log("BW:Creating browser window. ")
// avoid creating duplicate windows.
if (win) resolve();
if (win) win.show()
})
+ // For showing/hiding the splash screen.
win.webContents.on('did-finish-load', () => {
if (win) win.webContents.send('window-ready', {
message: true
import useMitt from "@/modules/mitt";
import { useIpc } from '@/modules/ipc';
import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
-import { renderMessage } from '@/modules/message';
+import { renderMessage, clientMessage } from '@/modules/message';
function showRecIcon () {
export default function useAudioInputController (typing: Ref) {
// For sending messages.
- const { post } = useIpc();
+ const { post, invoke } = useIpc();
const { emitter } = useMitt();
// Keepp track of when we are recording.
// Start recording on space bar.
if (cmd == "SPACE" && !recording.value && !typing.value) {
- post('update-recorder', {
- isRecording: true,
- uid: null
- });
+ post('start-recording', "");
showRecIcon()
recording.value = true;
}
- const onKeyUp = (e: KeyboardEvent) => {
+ const onKeyUp = async (e: KeyboardEvent) => {
const cmd = keyboardNameMap[e.keyCode];
// Stop recording on space up.
const message = renderMessage("", "", "", "sf")
emitter.emit("self-message", message);
- const audio = invoke('update-recorder', {
- isRecording: false,
- uid: message.uid
- });
+ // Stop recording and get audio from recorder.
+ const audio = await invoke('stop-recording', "");
+
+ // Send message to the backend for processing.
+ const clientM = clientMessage("", audio, message.uid);
+ post('client-message', clientM);
hideRecIcon()
recording.value = false;
<template>
- <form id="login" @submit.prevent="submit">
+ <form id="login" @submit.prevent="submitForm">
<!-- login title -->
<div id="loginBox">
<!-- username -->
<div class="inputBox">
- <input class="input" type="text" v-model="email" />
+ <input class="input" type="text" v-model="usr" />
<div class="inputModifier">Email</div>
</div>
<!-- password -->
<div class="inputBox">
- <input class="input" type="password" v-model="password" />
+ <input class="input" type="password" v-model="pwd" />
<div class="inputModifier">Password</div>
</div>
</div>
<!-- submit button; position: fixed -->
- <button class="submitButton button" type="submitForm">Submit</button>
-
- <div class="invalid" v-if="invalid">
- Incorrect Credentials
- </div>
+ <button class="submitButton button" type="submit">Submit</button>
+
</form>
<!-- back to login button: position: fixed -->
import { defineComponent, ref } from "vue";
import { useAuth } from "@/modules/auth";
import { useIpc } from "@/modules/ipc";
-import { useRouter } from "vue-router";
import useMessages from "@/modules/messages";
+import { authRequest } from '@/modules/message';
export default defineComponent({
name: "Login",
const { setToken } = useAuth();
const { resetMessages } = useMessages();
- const router = useRouter();
-
- const form = ref({
- email: "",
- password: "",
- })
+ let usr = ref("");
+ let pwd = ref("");
// Submit login credentials to the backend.
const submitForm = () => {
- post("auth-message", form)
+ console.log(`Submitting login form: ${usr.value}, ${pwd.value}`)
+ post("client-message", authRequest(false, usr.value, pwd.value))
}
return {
- form,
+ usr,
+ pwd,
submitForm
}
import { defineComponent, ref } from "vue";
import { useAuth } from "@/modules/auth";
import { useIpc } from "@/modules/ipc";
- import { useRouter } from "vue-router";
-
+ import { logoutRequest } from '@/modules/message';
export default defineComponent({
name: "Settings",
const toggleSettings = ref(false);
const { post } = useIpc();
- const { logout } = useAuth();
- const router = useRouter();
// Listen for escape key to close settings.
const onEscape = (e: any) => {
// We ask server to log us out.
const onLogout = () => {
- post("auth-request", "logout")
+ console.log("Submitting logout request.")
+ post("client-message", logoutRequest())
}
return {
-import { RenderMessage, ClientMessage } from "@/types/message/index";
+import { RenderMessage, ClientMessage, AuthRequest, LogoutRequest } from "@/types/message/index";
import { v4 as uuidv4 } from 'uuid';
function getTimeStamp(): number {
text,
uid
}
+)
+
+export const authRequest = (key: boolean | string, usr: boolean | string, pwd: boolean | string): AuthRequest => (
+ {
+ key,
+ usr,
+ pwd
+ }
+)
+
+export const logoutRequest = (): LogoutRequest => (
+ {
+ logout: true
+ }
)
\ No newline at end of file
// Run every time we want to connect to backend.
-export default function useWebSockets(receiveCallback: (s: string) => any) {
+export default function useWebSockets(receiveCallback: (s: string) => any, openCallback: () => any) {
- const sendMessage = (data: string) => {
+ // Returns bool (sucess or fail).
+ const sendMessage = (data: any) => {
+ console.log("WS:Sending message: ", data)
- console.log("Sending message: ", data)
-
- socket.send(data)
+ if (socket.readyState !== 1) {
+ return false
+ }
+
+ else {
+ socket.send(JSON.stringify(data))
+ return true
+ }
}
const onOpen = (event: WebSocket.OpenEvent) => {
- console.log('Connected to Server!');
+ console.log("WS:Connected to WS Server!");
+
+ openCallback()
}
const onServerMessage = (event: WebSocket.MessageEvent) => {
- console.log('Message received: ', event);
+ console.log("WS:Message received: ", event.data);
receiveCallback(event.data.toString())
}
const onClose = (event: WebSocket.CloseEvent) => {
- console.log("Socket closed normally.")
+ console.log("WS:Socket closed normally.")
}
// Reconnect automatically on error.
const onError = (event: WebSocket.ErrorEvent) => {
- console.log('WebSocket error: ', event);
+ console.log("WS:WebSocket error: ", event.message);
- console.log("Reconnecting...")
- createSocket()
+ console.log("Attempting reconnect in 1s.")
+ setTimeout(createSocket, 1000)
}
const createSocket = () => {
socket.addEventListener("open", onOpen)
socket.addEventListener("message", onServerMessage)
socket.addEventListener("close", onClose)
- socket.addEventListener("error", createSocket)
+ socket.addEventListener("error", onError)
- console.log("New socket created.")
+ console.log("WS:New socket created.")
}
return {
uid: string;
}
-export interface Creds {
- email: string;
- password: string;
+export interface AuthRequest {
+ key: boolean | string;
+ usr: boolean | string;
+ pwd: boolean | string;
+}
+
+export interface LogoutRequest {
+ logout: boolean;
}
// Possible messages from server:
+++ /dev/null
-<template>
- <div id="register">
- <!-- login title -->
- <div id="registerTitle">
- <div>Register</div>
- </div>
-
- <!-- username and password forms -->
- <form id="registerBox" @submit.prevent="submit">
- <!-- username -->
- <input class="input" type="text" v-model="email" placeholder="email" required/>
-
- <!-- password -->
- <input class="input" type="password" v-model="password" placeholder="Password" required/>
-
- <!-- re-enter passoword -->
- <input class="input" type="password" v-model="password2" placeholder="Re-enter password" required/>
-
- <!-- submit button; position: fixed -->
- <button class="button" type="submit">Submit</button>
-
- <div v-if="invalidEmail" class="invalid">Invalid Email.</div>
- <div v-else-if="weakPass" class="invalid">Password must be 8-15 characters long.
- <br>Password must have at least one:
- <br>  Upper case letter
- <br>  Lower case letter
- <br>  One numeric digit
- <br>  One special character
- </div>
- <div v-else-if="passUnMatch" class="invalid">Passwords dont match.</div>
-
- </form>
-
- <!-- back to login button: position: fixed -->
- <button class="button2" @click.prevent="switchView">Back to login</button>
-
- </div>
-</template>
-
-<script lang="ts">
-import {
- defineComponent,
- ref,
-} from "vue";
-import { useRouter } from "vue-router";
-import { useIpc } from "@/modules/ipc";
-import { useAuth } from "@/modules/auth";
-
-interface RegisterPayload {
- request: string;
- email: string;
- password: string;
-}
-
-export default defineComponent({
- name: "Register",
- setup() {
- const router = useRouter();
- const { invoke } = useIpc();
- const { setToken } = useAuth();
-
- const email = ref("");
- const password = ref("");
- const password2 = ref("");
- const invalidEmail = ref(false);
- const weakPass = ref(false);
- const passUnMatch = ref(false);
-
- // emai must be in '@email.com' format
- const emailReg = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
- // password must be 8-15 chars, have one upper, lower, number, and special char
- const pwReg = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,15}$/;
-
- // call this function on form submit.
- const submit = async () => {
- // reset invalid credentials references.
- invalidEmail.value = false;
- weakPass.value = false;
- passUnMatch.value = false;
-
- // validate email.
- if (!emailReg.test(email.value)) {
- invalidEmail.value = true;
- return;
- }
-
- // validate password strength.
- if (!pwReg.test(password.value)) {
- weakPass.value = true;
- return;
- }
-
- // check password match
- if (password.value !== password2.value) {
- passUnMatch.value = true;
- return;
- }
-
- const payload: RegisterPayload = {
- request: "register",
- email: email.value,
- password: password.value,
- };
-
- try {
- const res = await invoke('auth-session', payload);
- setToken(res);
- router.push({ name: "home" });
- } catch(e) {
- console.log('Failed to register.')
- }
-
- };
-
- const switchView = () => {
- router.push({ name: "login" });
- };
-
- return {
- email,
- password,
- password2,
- switchView,
- submit,
- invalidEmail,
- weakPass,
- passUnMatch,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-#register {
- width: 350px;
- height: 500px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background-color: #e6e6e6;
-}
-
-#registerTitle {
- min-width: 181px;
- font-size: 24px;
- font-weight: bold;
- text-align: left;
-}
-
-#registerBox {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
-}
-
-.input {
- background-color: #B9B9B9;
- border: none;
- color: white;
- padding: 16px;
- text-decoration: none;
- margin: 4px 2px;
- cursor: pointer;
- border-radius: 10px;
-}
-
-.button {
- position: fixed;
- margin-top: 165px;
- background-color: #58C4FD;
- border: none;
- color: white;
- padding: 10px 20px;
- text-decoration: none;
- border-radius: 20px;
- font-weight: bold;
-}
-
-.button2 {
- position: fixed;
- margin-top: 225px;
- border: none;
- background-color: #e6e6e6;
- text-decoration: none;
- color: #58C4FD;
- font-weight: bold;
-}
-
-.invalid {
- font-family: "SF Compact Display";
- font-weight: bold;
- font-size: 14px;
- color: #F53737;
-}
-</style>
-{"w":350,"h":699,"x":649,"y":168}
\ No newline at end of file
+{"w":629,"h":500,"x":1423,"y":657}
\ No newline at end of file