Merge remote-tracking branch 'origin/main' into new_architecture

This commit is contained in:
riqo 2021-06-19 11:30:29 -05:00
commit ab487e4f5e
9 changed files with 695 additions and 1 deletions

View file

@ -24,7 +24,6 @@
"animejs": "^3.2.0", "animejs": "^3.2.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"dotenv": "^10.0.0",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0",
"electron-store": "^8.0.0", "electron-store": "^8.0.0",
"electron-updater": "^4.3.8", "electron-updater": "^4.3.8",

212
src/App.vue Normal file
View file

@ -0,0 +1,212 @@
<template>
<!-- Only render when profile has been set -->
<div id="app" v-if="(ready)">
<button id="titlebar" />
<!-- Minimize and exit buttons. -->
<span class="menu">
<button
class="menuButton exitButton"
@click.prevent="post('nav-bar', 'close')"
/>
<button
class="menuButton minimizeButton"
@click.prevent="post('nav-bar', 'min')"
/>
</span>
<!-- Main Pages -->
<Messenger
v-if="profile"
:profile="profile"
:newMessages="newMessages"
/>
<Login v-else />
</div>
<!-- Show spash screen if ready is false -->
<Splash v-else />
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import { IpcRendererEvent } from "electron";
import { useIpc } from "@/modules/ipc";
import { useProfile } from "@/modules/auth"
import { invokeProfile } from "@/ipcRend/account";
import { postInitSession, postMount } from "@/ipcRend/session";
import Splash from "@/components/splash.vue";
import Messenger from "@/components/messenger.vue";
import Login from "@/components/login.vue";
import { Profile } from "@/types";
export default defineComponent({
components: {
Splash,
Messenger,
Login
},
setup() {
const { post, invoke } = useIpc();
const { profile, setProfile, clearProfile } = useProfile();
// Whether browser has received user info yet.
const ready = ref(false);
// New messages that browser missed while closed.
const newMessages = ref([]);
// Receive updated information about the session.
const updateState = (_event: IpcRendererEvent, payload: any) => {
console.log('YAYAYAYAYA');
console.log("APP:Received updated profile and new messages: \n" +
` profile: ${payload.message.profile}\n` +
` new: ${payload.message.newMessages}`);
if (payload.message.profile) {
console.log(`APP:Logged-in, showing Messenger View.`);
} else {
console.log(`APP:Logged-out, showing Login View.`);
}
newMessages.value = payload.message.newMessages;
ready.value = true;
}
const onSessionAuthFail = (_event: IpcRendererEvent, _payload: null) => {
clearProfile();
}
onMounted(async () => {
console.log("APP:mounted.");
window.ipcRenderer.on("session-auth-fail", onSessionAuthFail)
window.ipcRenderer.on("update-state", updateState)
window.ipcRenderer.on("update_available", () => {
console.log('testing auto update');
})
window.ipcRenderer.on("update_downloaded", () => {
console.log('testing update download');
})
postMount();
try {
const profile = await invokeProfile() as Profile;
setProfile(profile);
} catch(e) {
console.log('[REND] Auth:', e);
clearProfile();
} finally {
ready.value = true;
if (profile.value.crimataId) {
// start session
postInitSession();
}
}
});
onUnmounted(() => {
window.ipcRenderer.removeAllListeners("update-state");
window.ipcRenderer.removeAllListeners("session-auth-fail");
})
return {
ready,
post,
newMessages,
profile
}
}
})
</script>
<style lang="scss">
html, body {
margin: 0;
padding: 0;
// Background color set in window.ts
}
#app {
font-family: "SF Pro Text";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
height: 100vh;
width: 100vw;
border-radius: 15px;
}
#titlebar {
position: fixed;
width: 100vw;
height: 54px;
opacity: 0.75;
background-color: #EBEBEB;
-webkit-app-region: drag;
border: none;
outline: none;
z-index: 1;
}
.menu {
position: fixed;
margin-left: 20px;
margin-top: 20px;
z-index: 2;
}
.menuButton {
border: none;
outline:none;
min-width: 14px;
min-height: 14px;
border-radius: 7px;
}
.exitButton {
background-color: #FF6157;
}
.exitButton:active {
background: #c14645;
}
.minimizeButton {
background-color: #FFC12F;
margin-left: 8px;
}
.minimizeButton:active {
background-color: #c08e38;
}
</style>

13
src/authPayload.ts Normal file
View file

@ -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')
});

25
src/background.ts Normal file
View file

@ -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);
})();

View file

@ -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<Profile | Error> => (
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<Profile | Error> => (
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<void> => (
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);
}

View file

@ -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);
}

119
src/background/session.ts Normal file
View file

@ -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<string, any>): 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;
});
}

View file

@ -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<typeof setTimeout>;
// 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<string, any>): Promise<boolean> => (
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
}
}

23
src/ipcRend/session.ts Normal file
View file

@ -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)
);