diff --git a/package.json b/package.json
index 7fff91a..24d13b1 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,6 @@
"animejs": "^3.2.0",
"axios": "^0.21.1",
"core-js": "^3.6.5",
- "dotenv": "^10.0.0",
"electron-is-dev": "^2.0.0",
"electron-store": "^8.0.0",
"electron-updater": "^4.3.8",
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..99ffe6a
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/authPayload.ts b/src/authPayload.ts
new file mode 100644
index 0000000..61d68b6
--- /dev/null
+++ b/src/authPayload.ts
@@ -0,0 +1,13 @@
+
+import { store } from "@/background/store";
+
+interface PlatformAuthProtocol {
+ key: string;
+ crimata_id: string;
+}
+
+export const getAuthPayload = (): PlatformAuthProtocol => ({
+ key: store.get('key'),
+ crimata_id: store.get('crimataId')
+});
+
diff --git a/src/background.ts b/src/background.ts
new file mode 100644
index 0000000..977c265
--- /dev/null
+++ b/src/background.ts
@@ -0,0 +1,25 @@
+/*
+ * Entry point for Crimata electron app.
+ * "Look on my Works, ye Mighty, and despair!"
+ */
+
+"use strict";
+
+import { initApp } from './background/init';
+import { protocol } from "electron";
+
+// Scheme must be registered before the app is ready
+protocol.registerSchemesAsPrivileged([
+ { scheme: "app", privileges: { secure: true, standard: true } }
+]);
+
+// Load environment variable
+const isDev = require('electron-is-dev');
+
+// NOTE Program Begins Here
+(() => {
+
+ console.log('Starting Crimata electron app.');
+ initApp(isDev);
+
+})();
diff --git a/src/background/ipc/account.ts b/src/background/ipc/account.ts
new file mode 100644
index 0000000..4bf6411
--- /dev/null
+++ b/src/background/ipc/account.ts
@@ -0,0 +1,119 @@
+
+"use strict";
+
+import { Profile } from "@/types";
+import { submit, fetchProfile, logout } from "@/api/account";
+import { ipcMain, IpcMainInvokeEvent } from "electron";
+import { store } from "@/background/store";
+import { endSession } from "@/background/session";
+
+
+const parseAuthRes = (authRes: any) => {
+ const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
+ const profile = authRes.data as Profile;
+ return {
+ token,
+ profile
+ }
+};
+
+
+const onProfile = async (
+ _event: IpcMainInvokeEvent,
+ _payload: null
+): Promise => (
+
+ new Promise(async (resolve, reject) => {
+ console.log('[IPC]: user-profile');
+
+ // get jwt token and crimataId from store
+ const token = store.get('key');
+ const crimataId = store.get('crimataId');
+
+ // authenticate and fetch profile
+ try {
+
+ const res = await fetchProfile(
+ crimataId,
+ token
+ );
+
+ const parsed = parseAuthRes(res);
+ resolve(parsed.profile);
+
+ } catch(e) {
+ reject(new Error('Unable to authenticate and fetch account profile.'));
+ }
+ })
+)
+
+
+const onLogin = async (
+ _event: IpcMainInvokeEvent,
+ payload: string
+): Promise => (
+
+ new Promise(async (resolve, reject) => {
+ console.log('[IPC]: user-login');
+
+ const account = JSON.parse(payload);
+
+ if ( account.password && account.email ) {
+ try {
+ const res = await submit(account.email, account.password);
+ const parsed = parseAuthRes(res);
+
+ // save jwt token and profile
+ store.set('key', parsed.token);
+ store.set('crimataId', parsed.profile.crimataId);
+
+ // return profile to renderer
+ resolve(parsed.profile);
+
+ } catch(e) {
+ console.log('[API]', e);
+ reject(new Error('Failed to authenticate'));
+ }
+ }
+ })
+)
+
+const onLogout = async (
+ _event: IpcMainInvokeEvent,
+ _payload: null
+): Promise => (
+
+ new Promise(async (resolve, reject) => {
+ console.log('[IPC]: user-logout');
+
+ try {
+ // post logout to backend
+ await logout();
+
+ // remove key and crimataId
+ store.delete('key');
+ store.delete('crimataId');
+
+ // TODO: kill crimata platform session
+ endSession();
+
+ resolve();
+ } catch(e) {
+ reject(new Error('Failed to logout. Please try again.'));
+ }
+ })
+)
+
+
+export default function useAccountListeners(): void {
+
+ ipcMain.removeHandler("user-profile");
+ ipcMain.handle("user-profile", onProfile);
+
+ ipcMain.removeHandler("user-login");
+ ipcMain.handle("user-login", onLogin);
+
+ ipcMain.removeHandler("user-logout");
+ ipcMain.handle("user-logout", onLogout);
+
+}
diff --git a/src/background/ipc/session.ts b/src/background/ipc/session.ts
new file mode 100644
index 0000000..7c42ea2
--- /dev/null
+++ b/src/background/ipc/session.ts
@@ -0,0 +1,62 @@
+
+"use strict";
+
+import { initSession, emitNewMessages, sendMessage } from '@/background/session';
+import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
+import { initAudioIO } from "@/background/audio";
+import { ClientMessage } from "@/types";
+
+
+// Instantiate socket session with crimata-platorm.
+const onSessionInit = (
+ _event: IpcMainInvokeEvent,
+ _payload: null
+): void => {
+
+ console.log('[IPC]: init-session');
+
+ initSession();
+
+ initAudioIO();
+}
+
+
+const onAppMounted = (
+ _event: IpcMainInvokeEvent,
+ _payload: null
+): void => {
+
+ console.log('[IPC]: app-mounted');
+
+ emitNewMessages()
+};
+
+
+// Handle messages from window/client.
+const onClientMessage = (
+ _event: IpcMainEvent,
+ payload: ClientMessage
+): void => {
+
+ console.log('[IPC]: client-message');
+
+ sendMessage(payload);
+}
+
+
+export default function useSessionListeners(): void {
+
+ console.log('[IPC]: Init session listeners.');
+
+ // Attach listeners for frontend.
+ ipcMain.removeAllListeners("client-message");
+ ipcMain.on("client-message", onClientMessage);
+
+ // Attack browser window init listener.
+ ipcMain.removeAllListeners("app-mounted");
+ ipcMain.on("app-mounted", onAppMounted);
+
+ ipcMain.removeAllListeners("init-session");
+ ipcMain.on("init-session", onSessionInit);
+
+}
diff --git a/src/background/session.ts b/src/background/session.ts
new file mode 100644
index 0000000..c90d32a
--- /dev/null
+++ b/src/background/session.ts
@@ -0,0 +1,119 @@
+/*
+ * Creates a websocket session with Crimata Servers.
+ *
+ * Connects to Servers and attempts key authentication. Server will respond
+ * with key and user profile. We send the profile to the browser. We also
+ * resend this information on new broser window. We then serve as a
+ * communication interface between the window and the servers. It will
+ * automatically try to reconnect on websocket disconnect.
+ */
+
+import { backgroundMitt } from "@/modules/emitter";
+import { ipcEmit, loadState } from './helpers';
+import WebSocket from 'ws';
+
+import useWebSockets from "./websockets";
+
+import { play } from "./audio";
+import { renderMessage } from "@/modules/message";
+import { SessionState } from "@/types";
+
+let win = true;
+
+// Info saved to json on quit (key, newMessages).
+let state: SessionState;
+
+let socket: WebSocket | null = null;
+
+
+// Calls appropriate endpoint for a server message.
+const onMessage = (data: string): void => {
+ let message = JSON.parse(data);
+
+ if (message === "CLOSE_AUTH_FAIL") {
+ ipcEmit("session-auth-fail", null)
+ return;
+ }
+
+ // Standard message.
+ if (message.content) {
+
+ // Convert to render message
+ message = renderMessage(
+ message.content.text,
+ message.content.audio,
+ message.context,
+ message.modifier
+ );
+
+ if (win) {
+ console.log("SESS:Emitting standard message.")
+ if (message.audio) {
+ play(message.audio)
+ }
+ ipcEmit("render-message", message)
+ }
+
+ else {
+ console.log("SESS:No window: saving message.")
+ state.newMessages.push(message);
+ }
+ }
+
+ else {
+ if (win) {
+ ipcEmit("render-message", message)
+ }
+ }
+
+};
+
+
+// Websockets module.
+const { createSocket, send } = useWebSockets(onMessage);
+
+
+export const emitNewMessages = (): void => {
+ if (state) {
+ ipcEmit("update-state", {
+ newMessages: state.newMessages,
+ });
+ }
+
+}
+
+
+export const sendMessage = (payload: Record): void => {
+ try {
+ send(payload)
+ } catch(e) {
+ console.log("Unable to send message: ", payload);
+ }
+
+}
+
+export const endSession = (): void => {
+ if (socket) {
+ socket.close();
+ socket = null;
+ }
+}
+
+
+// Call this to initialize session with Crimata servers.
+export const initSession = (): void => {
+ console.log("SESS:Creating new session.")
+
+ // Load Json or createState.
+ state = loadState("session.json");
+
+ // Open socket connection.
+ if (!socket)
+ socket = createSocket();
+
+ // Keep win up-to-date.
+ backgroundMitt.on('window-active', (state: boolean) => {
+ win = state;
+ });
+
+}
diff --git a/src/background/websockets.ts b/src/background/websockets.ts
new file mode 100644
index 0000000..dec0203
--- /dev/null
+++ b/src/background/websockets.ts
@@ -0,0 +1,122 @@
+
+"use strict";
+
+import WebSocket from 'ws';
+import { getAuthPayload } from "./authPayload";
+import { ipcEmit } from './helpers';
+
+let socket: WebSocket;
+
+const socketUrl = "ws://127.0.0.1:8760"
+
+const _connectionCheckTimeout = 4000;
+const _reconnectTimeout = 1000;
+let _connectionCheckInterval: ReturnType;
+
+
+// Run every time we want to connect to backend.
+export default function useWebSockets(
+ receiveCallback: (s: string) => void,
+ openCallback?: () => void
+) {
+
+ // Returns bool (sucess or fail).
+ const sendMessage = (data: any) => {
+ console.log("WS:Sending message: ", data)
+
+ if (socket.readyState !== 1) {
+ return false
+ }
+
+ else {
+ socket.send(JSON.stringify(data))
+ return true
+ }
+
+ }
+
+ const send = async (data: Record): Promise => (
+ new Promise((resolve, reject) => {
+ if (socket.readyState !== 1) {
+ reject(false);
+ }
+ socket.send(JSON.stringify(data))
+ resolve(true);
+
+ }))
+
+
+ const onOpen = (_event: WebSocket.OpenEvent) => {
+
+ console.log("WS:Connected to WS Server!");
+ const jwt = getAuthPayload();
+ socket.send(JSON.stringify(jwt));
+
+ // ping server
+ _connectionCheckInterval = setInterval(() => {
+
+ if (socket) socket.ping(null, true, (e: Error) => {
+ if (e) {
+ ipcEmit('connection-alive', false);
+ socket.close();
+ setTimeout(createSocket, 1000);
+ }
+ });
+
+ }, _connectionCheckTimeout);
+
+ if (openCallback) openCallback();
+
+ }
+
+ const onServerMessage = (event: WebSocket.MessageEvent) => {
+
+ console.log("WS:Message received: ", event.data);
+
+ receiveCallback(event.data.toString())
+
+ }
+
+ const onClose = (event: WebSocket.CloseEvent) => {
+ console.log("WS:Socket closed normally.", event.wasClean)
+
+ clearInterval(_connectionCheckInterval);
+
+ if (!event.wasClean) {
+ ipcEmit('connection-alive', false);
+ setTimeout(createSocket, 1000);
+ }
+ }
+
+ // Reconnect automatically on error.
+ const onError = (event: WebSocket.ErrorEvent) => {
+ console.log("WS:WebSocket error: ", event.message);
+ }
+
+
+ const createSocket = (): WebSocket => {
+
+ if (_connectionCheckInterval) clearInterval(_connectionCheckInterval);
+
+ socket = new WebSocket(socketUrl);
+
+ // Add listeners.
+ socket.addEventListener("open", onOpen);
+ socket.addEventListener("message", onServerMessage);
+ socket.addEventListener("close", onClose);
+ socket.addEventListener("error", onError);
+ socket.addEventListener("pong", () => {
+ ipcEmit('connection-alive', true);
+ });
+
+ return socket;
+
+ }
+
+ return {
+ createSocket,
+ sendMessage,
+ send
+ }
+
+}
diff --git a/src/ipcRend/session.ts b/src/ipcRend/session.ts
new file mode 100644
index 0000000..a5ff5fc
--- /dev/null
+++ b/src/ipcRend/session.ts
@@ -0,0 +1,23 @@
+
+import { useIpc } from "@/modules/ipc";
+
+import { ClientMessage } from "@/types";
+
+const { post } = useIpc();
+
+
+export const postMount = (): void => (
+ post("app-mounted", null)
+);
+
+
+export const postInitSession = (): void => (
+ post("init-session", null)
+);
+
+
+export const postMessage = (payload: ClientMessage): void => (
+ post('client-message', payload)
+);
+
+