2024-07-07 19:20:51 +02:00
|
|
|
import * as React from "bloatless-react";
|
|
|
|
|
|
2024-07-08 00:39:22 +02:00
|
|
|
import {
|
|
|
|
|
arrayToUint8,
|
|
|
|
|
decode,
|
|
|
|
|
decrypt,
|
|
|
|
|
encrypt,
|
|
|
|
|
generateIV,
|
|
|
|
|
importKey,
|
|
|
|
|
splitMergedString,
|
|
|
|
|
uInt8ToArray,
|
|
|
|
|
} from "./cryptUtility";
|
|
|
|
|
|
2024-07-07 19:20:51 +02:00
|
|
|
import UDNFrontend from "udn-frontend";
|
2024-07-08 00:39:22 +02:00
|
|
|
import { translation } from "./translations";
|
2024-07-07 19:20:51 +02:00
|
|
|
|
|
|
|
|
const UDN = new UDNFrontend();
|
|
|
|
|
|
|
|
|
|
// TYPE
|
|
|
|
|
export class Channel implements React.Identifiable {
|
|
|
|
|
id = React.UUID();
|
|
|
|
|
constructor(public channelName: string) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Message extends React.Identifiable {
|
|
|
|
|
channel: string;
|
|
|
|
|
sender: string;
|
|
|
|
|
body: string;
|
|
|
|
|
isoDate: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class Message implements React.Identifiable {
|
|
|
|
|
id = React.UUID();
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
public channel: string,
|
|
|
|
|
public sender: string,
|
|
|
|
|
public body: string,
|
|
|
|
|
public isoDate: string
|
|
|
|
|
) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// STATE
|
|
|
|
|
// connection
|
2024-07-07 22:11:18 +02:00
|
|
|
export const currentAddress = new React.State("");
|
|
|
|
|
export const serverAddress = React.restoreState("socket-address", "");
|
2024-07-07 19:20:51 +02:00
|
|
|
export const isConnected = new React.State(false);
|
2024-07-08 00:48:58 +02:00
|
|
|
export const connectionMessage = React.createProxyState([serverAddress], () =>
|
|
|
|
|
translation.connectedTo(serverAddress.value)
|
|
|
|
|
);
|
2024-07-07 19:20:51 +02:00
|
|
|
|
2024-07-08 13:35:26 +02:00
|
|
|
export const cannotDisonnect = React.createProxyState(
|
|
|
|
|
[isConnected],
|
|
|
|
|
() => isConnected.value == false
|
|
|
|
|
);
|
|
|
|
|
|
2024-07-07 22:11:18 +02:00
|
|
|
export const cannotConnect = React.createProxyState(
|
2024-07-08 00:39:22 +02:00
|
|
|
[serverAddress, currentAddress, isConnected],
|
2024-07-07 22:11:18 +02:00
|
|
|
() =>
|
|
|
|
|
serverAddress.value == "" ||
|
|
|
|
|
(currentAddress.value == serverAddress.value && isConnected.value == true)
|
|
|
|
|
);
|
|
|
|
|
|
2024-07-09 00:28:07 +02:00
|
|
|
// mailbox
|
|
|
|
|
export const mailboxId = React.restoreState("mailbox-id", "");
|
|
|
|
|
export const isMailboxConnected = new React.State(false);
|
|
|
|
|
|
|
|
|
|
export const cannotRequestMailbox = React.createProxyState(
|
|
|
|
|
[isConnected, isMailboxConnected],
|
|
|
|
|
() => isConnected.value == false || isMailboxConnected.value == true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const cannotDeleteMailbox = React.createProxyState(
|
|
|
|
|
[isConnected, mailboxId, isMailboxConnected],
|
|
|
|
|
() =>
|
|
|
|
|
isConnected.value == false ||
|
|
|
|
|
mailboxId.value == "" ||
|
|
|
|
|
isMailboxConnected.value == false
|
|
|
|
|
);
|
|
|
|
|
|
2024-07-08 00:39:22 +02:00
|
|
|
// encryption
|
|
|
|
|
export const encryptionKey = React.restoreState("encryption-key", "");
|
|
|
|
|
export const isEncryptionUnavailable = window.crypto.subtle == undefined;
|
|
|
|
|
|
2024-07-09 00:28:07 +02:00
|
|
|
// channel
|
2024-07-07 22:11:18 +02:00
|
|
|
export const currentPrimaryChannel = new React.State("");
|
2024-07-07 19:20:51 +02:00
|
|
|
export const primaryChannel = React.restoreState("primary-channel", "");
|
2024-07-08 02:44:23 +02:00
|
|
|
|
2024-07-07 22:11:18 +02:00
|
|
|
export const newSecondaryChannelName = new React.State("");
|
|
|
|
|
export const secondaryChannels =
|
|
|
|
|
React.restoreListState<Channel>("secondary-channels");
|
2024-07-08 02:44:23 +02:00
|
|
|
|
2024-07-07 22:11:18 +02:00
|
|
|
export const cannotSetChannel = React.createProxyState(
|
|
|
|
|
[primaryChannel, currentPrimaryChannel, isConnected],
|
2024-07-08 02:44:23 +02:00
|
|
|
() => primaryChannel.value == "" || isConnected.value == false
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const cannotLeaveChannel = React.createProxyState(
|
|
|
|
|
[currentPrimaryChannel, isConnected],
|
|
|
|
|
() => currentPrimaryChannel.value == "" || isConnected.value == false
|
2024-07-07 22:11:18 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const cannotAddSecondaryChannel = React.createProxyState(
|
|
|
|
|
[newSecondaryChannelName],
|
|
|
|
|
() => newSecondaryChannelName.value == ""
|
|
|
|
|
);
|
2024-07-07 19:20:51 +02:00
|
|
|
|
2024-07-09 00:28:07 +02:00
|
|
|
// sending
|
|
|
|
|
export const senderName = React.restoreState("sender-name", "");
|
|
|
|
|
|
|
|
|
|
export const messageBody = React.restoreState("message", "");
|
|
|
|
|
|
2024-07-07 19:20:51 +02:00
|
|
|
export const cannotSendMessage = React.createProxyState(
|
2024-07-08 13:35:26 +02:00
|
|
|
[currentPrimaryChannel, messageBody, senderName, isConnected],
|
2024-07-07 22:11:18 +02:00
|
|
|
() =>
|
2024-07-08 13:35:26 +02:00
|
|
|
isConnected.value == false ||
|
2024-07-07 22:11:18 +02:00
|
|
|
currentPrimaryChannel.value == "" ||
|
|
|
|
|
messageBody.value == "" ||
|
|
|
|
|
senderName.value == ""
|
2024-07-07 19:20:51 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// receiving
|
|
|
|
|
export const messages = React.restoreListState<Message>("messages");
|
|
|
|
|
|
|
|
|
|
// METHODS
|
2024-07-09 00:28:07 +02:00
|
|
|
// connection
|
|
|
|
|
export function connect(): void {
|
2024-07-07 22:11:18 +02:00
|
|
|
if (cannotConnect.value == true) return;
|
|
|
|
|
currentAddress.value = serverAddress.value;
|
2024-07-08 00:39:22 +02:00
|
|
|
isConnected.value = false;
|
2024-07-07 22:11:18 +02:00
|
|
|
UDN.connect(serverAddress.value);
|
2024-07-07 19:20:51 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-09 00:28:07 +02:00
|
|
|
export function disconnect(): void {
|
2024-07-08 13:35:26 +02:00
|
|
|
if (cannotDisonnect.value == true) return;
|
|
|
|
|
UDN.disconnect();
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 00:28:07 +02:00
|
|
|
// mailbox
|
|
|
|
|
export function requestMailbox(): void {
|
|
|
|
|
UDN.requestMailbox();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function deleteMailbox(): void {
|
|
|
|
|
UDN.deleteMailbox(mailboxId.value);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 03:24:38 +02:00
|
|
|
export function updateMailbox(): void {
|
2024-07-09 03:32:22 +02:00
|
|
|
if (!isMailboxConnected || mailboxId.value == "") return;
|
|
|
|
|
|
2024-07-09 03:24:38 +02:00
|
|
|
deleteMailbox();
|
|
|
|
|
requestMailbox();
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 00:28:07 +02:00
|
|
|
// messages
|
2024-07-08 00:39:22 +02:00
|
|
|
export async function sendMessage(): Promise<void> {
|
2024-07-07 19:20:51 +02:00
|
|
|
if (cannotSendMessage.value == true) return;
|
|
|
|
|
|
|
|
|
|
// get channels
|
2024-07-07 22:11:18 +02:00
|
|
|
const secondaryChannelNames: string[] = [
|
|
|
|
|
...secondaryChannels.value.values(),
|
2024-07-07 19:20:51 +02:00
|
|
|
].map((channel) => channel.channelName);
|
|
|
|
|
const allChannelNames: string[] = [
|
|
|
|
|
primaryChannel.value,
|
2024-07-07 22:11:18 +02:00
|
|
|
...secondaryChannelNames,
|
2024-07-07 19:20:51 +02:00
|
|
|
];
|
|
|
|
|
|
2024-07-08 00:39:22 +02:00
|
|
|
// encrypt
|
|
|
|
|
const encrypted = await encryptMessage();
|
|
|
|
|
if (encrypted == undefined) return;
|
|
|
|
|
|
2024-07-07 19:20:51 +02:00
|
|
|
// create object
|
|
|
|
|
const joinedChannelName = allChannelNames.join("/");
|
|
|
|
|
const messageObject = new Message(
|
|
|
|
|
joinedChannelName,
|
|
|
|
|
senderName.value,
|
2024-07-08 00:39:22 +02:00
|
|
|
encrypted,
|
2024-07-07 19:20:51 +02:00
|
|
|
new Date().toISOString()
|
|
|
|
|
);
|
|
|
|
|
const messageString = JSON.stringify(messageObject);
|
|
|
|
|
|
2024-07-07 22:11:18 +02:00
|
|
|
// send
|
2024-07-07 19:20:51 +02:00
|
|
|
UDN.sendMessage(joinedChannelName, messageString);
|
2024-07-07 22:11:18 +02:00
|
|
|
|
|
|
|
|
// clear
|
|
|
|
|
messageBody.value = "";
|
2024-07-07 19:20:51 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-08 12:34:37 +02:00
|
|
|
export function clearMessageHistory(): void {
|
2024-07-08 00:39:22 +02:00
|
|
|
messages.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 12:34:37 +02:00
|
|
|
export function deleteMessage(message: Message): void {
|
|
|
|
|
messages.remove(message);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 00:28:07 +02:00
|
|
|
// channels
|
2024-07-07 19:20:51 +02:00
|
|
|
export function setChannel(): void {
|
2024-07-07 22:11:18 +02:00
|
|
|
if (cannotSetChannel.value == true) return;
|
|
|
|
|
|
|
|
|
|
if (currentPrimaryChannel != undefined) {
|
|
|
|
|
UDN.unsubscribe(currentPrimaryChannel.value);
|
2024-07-07 19:20:51 +02:00
|
|
|
}
|
|
|
|
|
UDN.subscribe(primaryChannel.value);
|
2024-07-09 03:32:22 +02:00
|
|
|
|
2024-07-09 03:24:38 +02:00
|
|
|
updateMailbox();
|
2024-07-07 19:20:51 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-08 02:44:23 +02:00
|
|
|
export function leaveChannel(): void {
|
|
|
|
|
if (cannotLeaveChannel.value == true) return;
|
|
|
|
|
|
|
|
|
|
UDN.unsubscribe(currentPrimaryChannel.value);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 22:11:18 +02:00
|
|
|
export function addSecondaryChannel(): void {
|
|
|
|
|
if (cannotAddSecondaryChannel.value == true) return;
|
|
|
|
|
|
|
|
|
|
const channelObject = new Channel(newSecondaryChannelName.value);
|
|
|
|
|
secondaryChannels.add(channelObject);
|
|
|
|
|
newSecondaryChannelName.value = "";
|
2024-07-07 19:20:51 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-07 22:11:18 +02:00
|
|
|
export function removeSecondaryChannel(channel: Channel): void {
|
|
|
|
|
secondaryChannels.remove(channel);
|
2024-07-07 19:20:51 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-09 00:28:07 +02:00
|
|
|
// encryption
|
|
|
|
|
export async function decryptReceivedMessage(message: Message): Promise<void> {
|
|
|
|
|
message.body = await decryptMessage(message.body);
|
|
|
|
|
messages.callSubscriptions();
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 19:20:51 +02:00
|
|
|
// LISTENERS
|
|
|
|
|
UDN.onconnect = () => {
|
|
|
|
|
isConnected.value = true;
|
2024-07-08 00:39:22 +02:00
|
|
|
setChannel();
|
2024-07-09 00:28:07 +02:00
|
|
|
|
|
|
|
|
if (mailboxId.value != "") {
|
|
|
|
|
UDN.connectMailbox(mailboxId.value);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-07-09 03:32:22 +02:00
|
|
|
UDN.onmailboxcreate = (id) => {
|
2024-07-09 00:28:07 +02:00
|
|
|
mailboxId.value = id;
|
|
|
|
|
UDN.connectMailbox(id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UDN.onmailboxconnect = () => {
|
|
|
|
|
isMailboxConnected.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UDN.onmailboxdelete = () => {
|
|
|
|
|
isMailboxConnected.value = false;
|
|
|
|
|
mailboxId.value = "";
|
2024-07-07 19:20:51 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UDN.onmessage = (data) => {
|
2024-07-07 22:11:18 +02:00
|
|
|
if (data.subscribed != undefined && data.messageChannel != undefined) {
|
2024-07-07 19:20:51 +02:00
|
|
|
return handleSubscriptionConfirmation(data.messageChannel, data.subscribed);
|
|
|
|
|
} else if (data.messageBody) {
|
|
|
|
|
handleMessage(data.messageBody);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UDN.ondisconnect = () => {
|
|
|
|
|
isConnected.value = false;
|
2024-07-09 00:28:07 +02:00
|
|
|
isMailboxConnected.value = false;
|
2024-07-07 19:20:51 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// UTILITY
|
|
|
|
|
function handleSubscriptionConfirmation(
|
|
|
|
|
channel: string,
|
|
|
|
|
isSubscribed: boolean
|
|
|
|
|
): void {
|
|
|
|
|
if (isSubscribed == true) {
|
2024-07-07 22:11:18 +02:00
|
|
|
currentPrimaryChannel.value = channel;
|
2024-07-08 02:44:23 +02:00
|
|
|
} else {
|
|
|
|
|
currentPrimaryChannel.value = "";
|
2024-07-07 19:20:51 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 00:39:22 +02:00
|
|
|
async function handleMessage(body: string): Promise<void> {
|
2024-07-07 19:20:51 +02:00
|
|
|
try {
|
|
|
|
|
const object: Message = JSON.parse(body);
|
|
|
|
|
if (!object.channel || !object.sender || !object.body || !object.isoDate)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const messageObject = new Message(
|
|
|
|
|
object.channel,
|
|
|
|
|
object.sender,
|
2024-07-08 00:39:22 +02:00
|
|
|
await decryptMessage(object.body),
|
2024-07-07 19:20:51 +02:00
|
|
|
object.isoDate
|
|
|
|
|
);
|
|
|
|
|
messages.add(messageObject);
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
2024-07-08 00:39:22 +02:00
|
|
|
|
|
|
|
|
async function encryptMessage(): Promise<string> {
|
|
|
|
|
if (encryptionKey.value == "") return messageBody.value;
|
|
|
|
|
|
|
|
|
|
if (!window.crypto.subtle) return messageBody.value;
|
|
|
|
|
|
|
|
|
|
const iv = generateIV();
|
|
|
|
|
const key = await importKey(encryptionKey.value, "encrypt");
|
|
|
|
|
const encryptedArray = await encrypt(iv, key, messageBody.value);
|
|
|
|
|
|
|
|
|
|
const encryptionData = {
|
|
|
|
|
iv: uInt8ToArray(iv),
|
|
|
|
|
encryptedArray: uInt8ToArray(encryptedArray),
|
|
|
|
|
};
|
|
|
|
|
return btoa(JSON.stringify(encryptionData));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function decryptMessage(encryptionData: string): Promise<string> {
|
|
|
|
|
try {
|
|
|
|
|
const encrypionData = JSON.parse(atob(encryptionData));
|
|
|
|
|
const iv = arrayToUint8(encrypionData.iv);
|
|
|
|
|
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
|
|
|
|
|
|
|
|
|
|
const key = await importKey(encryptionKey.value, "decrypt");
|
|
|
|
|
return await decrypt(iv, key, encryptedArray);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
return encryptionData;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-09 00:28:07 +02:00
|
|
|
|
|
|
|
|
// INIT
|
|
|
|
|
if (serverAddress.value != "") {
|
|
|
|
|
connect();
|
|
|
|
|
}
|