mirror of
https://github.com/Discreetly/frontend.git
synced 2026-01-08 20:38:04 -05:00
the word
This commit is contained in:
19
package-lock.json
generated
19
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"date-fns": "^2.30.0",
|
||||
"discreetly-interfaces": "^0.1.39",
|
||||
"dompurify": "^3.0.5",
|
||||
"idc-nullifier": "^0.0.4",
|
||||
"libsodium-wrappers": "^0.7.11",
|
||||
"marked": "^7.0.5",
|
||||
"node-emoji": "^2.1.0",
|
||||
@@ -4070,6 +4071,11 @@
|
||||
"wasmbuilder": "0.0.16"
|
||||
}
|
||||
},
|
||||
"node_modules/circomlib": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/circomlib/-/circomlib-2.0.5.tgz",
|
||||
"integrity": "sha512-O7NQ8OS+J4eshBuoy36z/TwQU0YHw8W3zxZcs4hVwpEll3e4hDm3mgkIPqItN8FDeLEKZFK3YeT/+k8TiLF3/A=="
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
@@ -5913,6 +5919,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/idc-nullifier": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/idc-nullifier/-/idc-nullifier-0.0.4.tgz",
|
||||
"integrity": "sha512-kQvqvagzSbrMDcwmmHqw7sF6G0GHqQLJqyec/ie3v91Bo8Sjp/zXOczbE+4b2hGHUo8CnMaCL+P+ozKPh02pKg==",
|
||||
"dependencies": {
|
||||
"@semaphore-protocol/identity": "^3.10.1",
|
||||
"circomlib": "^2.0.5",
|
||||
"poseidon-lite": "^0.2.0",
|
||||
"snarkjs": "^0.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -10582,4 +10599,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"date-fns": "^2.30.0",
|
||||
"discreetly-interfaces": "^0.1.39",
|
||||
"dompurify": "^3.0.5",
|
||||
"idc-nullifier": "^0.0.4",
|
||||
"libsodium-wrappers": "^0.7.11",
|
||||
"marked": "^7.0.5",
|
||||
"node-emoji": "^2.1.0",
|
||||
@@ -83,4 +84,4 @@
|
||||
"viem": "^1.16.5",
|
||||
"wagmi": "^1.4.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
src/lib/components/Gateways/TheWord.svelte
Normal file
67
src/lib/components/Gateways/TheWord.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { alertQueue } from '$lib/stores';
|
||||
import { FileDropzone } from '@skeletonlabs/skeleton';
|
||||
import Loading from '../Utils/Loading.svelte';
|
||||
import { theWordRequest } from '$lib/gateways/theWord';
|
||||
let files: FileList;
|
||||
let loading = false;
|
||||
|
||||
function theWordHandler(proof: any) {
|
||||
loading = true;
|
||||
theWordRequest(proof)
|
||||
.then(({ acceptedRoomNames, err }) => {
|
||||
if (err) {
|
||||
alertQueue.enqueue(err, 'error');
|
||||
} else {
|
||||
alertQueue.enqueue(`Accepted into ${acceptedRoomNames}`, 'success');
|
||||
acceptedRoomNames = acceptedRoomNames;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
alertQueue.enqueue(err, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeHandler(e: Event): void {
|
||||
const f = files.item(0);
|
||||
console.debug(`Proof File type detected as ${f?.type}`);
|
||||
let unverifiedData: any;
|
||||
if (!f) {
|
||||
alertQueue.enqueue('No file selected', 'warning');
|
||||
return;
|
||||
}
|
||||
if (f.type == 'application/json' || f.type == 'text/plain') {
|
||||
f.text().then((text) => {
|
||||
unverifiedData = JSON.parse(text);
|
||||
theWordHandler(unverifiedData);
|
||||
});
|
||||
} else {
|
||||
alertQueue.enqueue(
|
||||
'Invalid file type, must be a JSON object in the SNARKProof format',
|
||||
'warning'
|
||||
);
|
||||
console.warn('Invalid file type');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3 justify-between">
|
||||
<section class="px-4 pt-4">
|
||||
<label class="label w-full pb-1">
|
||||
<span class="h5"> Prove From File:</span>
|
||||
<FileDropzone
|
||||
class="px-2"
|
||||
name="the-word-proof.json"
|
||||
bind:files
|
||||
on:change={onChangeHandler}
|
||||
/></label
|
||||
>
|
||||
</section>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import EthereumGroupGateway from '$lib/components/Gateways/EthereumGroup.svelte';
|
||||
import InviteCodeGateway from '$lib/components/Gateways/InviteCode.svelte';
|
||||
import TheWord from '$lib/components/Gateways/TheWord.svelte';
|
||||
import SelectServer from '$lib/components/Server/SelectServer.svelte';
|
||||
import Card from '$lib/components/Utils/Card.svelte';
|
||||
import { configStore, serverStore } from '$lib/stores';
|
||||
@@ -45,4 +46,12 @@
|
||||
</svelte:fragment>
|
||||
<EthereumGroupGateway />
|
||||
</Card>
|
||||
<Card>
|
||||
<svelte:fragment slot="header">The Word</svelte:fragment>
|
||||
<svelte:fragment slot="description"
|
||||
>Do you know <a class="link" href="https://github.com/Mach-34/the-word/">the word</a>? Prove
|
||||
it.
|
||||
</svelte:fragment>
|
||||
<TheWord />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
41
src/lib/gateways/theWord.ts
Normal file
41
src/lib/gateways/theWord.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getCommitment, updateRooms } from '$lib/utils/';
|
||||
import { postTheWord } from '$lib/services/server';
|
||||
import { selectedServer, configStore, alertQueue } from '$lib/stores';
|
||||
import { get } from 'svelte/store';
|
||||
import type { JoinResponseI, SNARKProof } from '$lib/types';
|
||||
|
||||
export async function theWordRequest(proof: SNARKProof) {
|
||||
let acceptedRoomNames: string[] = [];
|
||||
let err: string | undefined;
|
||||
const server = get(selectedServer);
|
||||
try {
|
||||
const idc = getCommitment();
|
||||
if (!idc) {
|
||||
alertQueue.enqueue('No identity commitment found', 'warning');
|
||||
throw new Error('No identity commitment found');
|
||||
}
|
||||
const result = (await postTheWord(server, {
|
||||
proof,
|
||||
idc
|
||||
})) as JoinResponseI;
|
||||
console.debug('INVITE CODE RESPONSE: ', result);
|
||||
if (result.status == 'valid') {
|
||||
console.debug('Updating new rooms');
|
||||
acceptedRoomNames = await updateRooms(server, result.roomIds);
|
||||
console.log(`Added to rooms: ${acceptedRoomNames}`);
|
||||
configStore.update((store) => {
|
||||
store['signUpStatus']['completedSignup'] = true;
|
||||
store['signUpStatus']['inviteCode'] = '';
|
||||
return store;
|
||||
});
|
||||
return { acceptedRoomNames, undefined };
|
||||
} else if (result.status == 'already-added') {
|
||||
return { acceptedRoomNames, err: 'You are already a member of this room.' };
|
||||
} else {
|
||||
err = 'Invalid invite code.';
|
||||
}
|
||||
} catch (e) {
|
||||
err = String((e as Error).message);
|
||||
}
|
||||
return { acceptedRoomNames, err };
|
||||
}
|
||||
@@ -47,6 +47,27 @@ export async function get(urlParts: string[] | string): Promise<object> {
|
||||
throw new Error(`Failed to fetch ${url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - makes a get request to the api
|
||||
* @param {string[] | string} urlParts - the url parts to be joined to form the url
|
||||
* @returns {object} - the response from the api
|
||||
* @throws {Error} - if the request fails
|
||||
*/
|
||||
export async function getWithData(urlParts: string[] | string, data: object): Promise<object> {
|
||||
const url = cleanURL(urlParts);
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
throw new Error(`Failed to fetch ${url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - makes a get request to the api
|
||||
* @param {string[] | string} urlParts - the url parts to be joined to form the url
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import type { MessageI, ServerI } from 'discreetly-interfaces';
|
||||
import type { Invites, RoomI } from '$lib/types';
|
||||
|
||||
import { get, post, postAuth } from './api';
|
||||
import { Prover } from 'idc-nullifier';
|
||||
import type { Identity } from '@semaphore-protocol/identity';
|
||||
import { get, getWithData, post, postAuth } from './api';
|
||||
import { getIdentity } from '$lib/utils';
|
||||
import { alertQueue } from '$lib/stores';
|
||||
|
||||
export async function getIdentityRoomIds(server: string, idCommitment: string): Promise<string[]> {
|
||||
return get([server, `api/rooms/${idCommitment}`]) as Promise<string[]>;
|
||||
const prover = new Prover();
|
||||
const id = getIdentity() as unknown as Identity;
|
||||
if (id) {
|
||||
// Proves you know the identity secret with a timestamp so this proof can't be replayed
|
||||
const proof = prover.generateProof({
|
||||
identity: id,
|
||||
externalNullifier: BigInt(Date.now())
|
||||
});
|
||||
return getWithData([server, `api/rooms/${idCommitment}`], proof) as Promise<string[]>;
|
||||
} else {
|
||||
alertQueue.enqueue('No identity found when fetching rooms', 'error');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRoomById(server: string, roomId: string): Promise<RoomI> {
|
||||
@@ -26,6 +41,10 @@ export async function postInviteCode(serverUrl: string, data: { code: string; id
|
||||
return post([serverUrl, 'join'], data);
|
||||
}
|
||||
|
||||
export async function postTheWord(serverUrl: string, data: { proof: object; idc: string }) {
|
||||
return post([serverUrl, 'theword'], data);
|
||||
}
|
||||
|
||||
export async function getMessages(serverUrl: string, roomId: string) {
|
||||
return get([serverUrl, `api/room/${roomId}/messages`]) as Promise<MessageI[]>;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import type {
|
||||
roomStoreI,
|
||||
selectedRoomStoreI,
|
||||
serverStoreI,
|
||||
roomKeyStoreI,
|
||||
keyStoreI
|
||||
keyStoreI,
|
||||
roomPassStoreI,
|
||||
roomKeyStoreI
|
||||
} from '$lib/types';
|
||||
|
||||
/* ------------------ Server State ------------------*/
|
||||
@@ -64,7 +65,7 @@ export const pixelStore = sessionable({} as pixelStoreI, 'pixelmaps');
|
||||
/**
|
||||
* @description Stores the encrypted key for each room keyed by the roomId
|
||||
*/
|
||||
export const roomKeyStore = encryptable({} as roomKeyStoreI, 'roomKey');
|
||||
export const roomPassStore = encryptable({} as roomPassStoreI, 'roomKey');
|
||||
|
||||
/**
|
||||
* @description Derived Store: The messages of the currently selected room
|
||||
@@ -95,6 +96,7 @@ export const saltStore = storable('', 'salt');
|
||||
* !WARN NEVER CHANGE THE STORE TYPE OR YOU RISK EXPOSING THE KEY
|
||||
*/
|
||||
export const keyStore = writable({} as keyStoreI);
|
||||
export const roomKeyStore = writable({} as roomKeyStoreI);
|
||||
|
||||
export const alertQueue = queueable([]);
|
||||
|
||||
|
||||
@@ -53,3 +53,8 @@ export interface RoomFormData {
|
||||
bandadaGroupId: string | undefined;
|
||||
bandadaApiKey: string | undefined;
|
||||
}
|
||||
|
||||
export interface SNARKProof {
|
||||
proof: object;
|
||||
publicSignals: string[];
|
||||
}
|
||||
|
||||
@@ -55,10 +55,14 @@ export interface consoleStoreI {
|
||||
};
|
||||
}
|
||||
|
||||
export interface roomKeyStoreI {
|
||||
export interface roomPassStoreI {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface roomKeyStoreI {
|
||||
[key: string]: CryptoKey;
|
||||
}
|
||||
|
||||
export type keyStoreI = CryptoKey | undefined | null;
|
||||
|
||||
export interface EncryptableT<Type> extends Writable<Type> {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
keyStore,
|
||||
identityExists,
|
||||
alertQueue,
|
||||
roomKeyStore
|
||||
roomPassStore
|
||||
} from '$lib/stores';
|
||||
import { encryptIdentity } from './identity';
|
||||
import type { IdentityStoreI } from '$lib/types';
|
||||
@@ -70,7 +70,7 @@ export async function unlockPadlock(password: string) {
|
||||
// encrypted stores, so we have to trigger it manually
|
||||
// when the keys are derived
|
||||
identityKeyStore.read();
|
||||
roomKeyStore.read();
|
||||
roomPassStore.read();
|
||||
});
|
||||
} else {
|
||||
alertQueue.enqueue('Incorrect Password', 'warning');
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
{messageId}
|
||||
{messagesLeft}
|
||||
{roomRateLimit}
|
||||
{onlineMembers}
|
||||
/>
|
||||
{#if $configStore.experience == Experiences.Chat}
|
||||
{#key $currentSelectedRoom.roomId}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { currentSelectedRoom, configStore } from '$lib/stores';
|
||||
import { ProgressBar } from '@skeletonlabs/skeleton';
|
||||
import FullCircle from 'svelte-material-icons/Circle.svelte';
|
||||
import Person from 'svelte-material-icons/Account.svelte';
|
||||
import ExperienceMenu from './ExperienceMenu.svelte';
|
||||
export let connected: boolean;
|
||||
export let currentEpoch: number;
|
||||
@@ -12,7 +13,10 @@
|
||||
export let roomRateLimit: number;
|
||||
export let messagesLeft: () => number;
|
||||
export let messageId: number;
|
||||
export let onlineMembers: string;
|
||||
$: roomId = $currentSelectedRoom?.roomId!.toString();
|
||||
$: encrypted = $currentSelectedRoom?.encrypted ?? false;
|
||||
$: ephemeral = $currentSelectedRoom?.ephemeral ?? false;
|
||||
$: roomName = $currentSelectedRoom?.name ?? 'Select Room';
|
||||
$: epochLengthSeconds = roomRateLimit / 1000;
|
||||
$: timeToNextEpoch = epochLengthSeconds - +timeLeftInEpoch;
|
||||
@@ -31,12 +35,30 @@
|
||||
{/if}
|
||||
</span>
|
||||
<h2 class="h5 text-secondary-800-100-token" title={roomId}>
|
||||
{#if encrypted}
|
||||
🔒
|
||||
{/if}
|
||||
{#if ephemeral}
|
||||
⏳
|
||||
{/if}
|
||||
{roomName}
|
||||
</h2>
|
||||
<div class="hidden sm:block ms-2 text-xs font-mono self-center">
|
||||
[{timeToNextEpoch.toFixed(1)}/{epochLengthSeconds}s]
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
{#if !connected}
|
||||
<span
|
||||
class:connected
|
||||
class="flex flex-row align-middle text-sm font-mono text-secondary-300-600-token"
|
||||
title="Online Users"
|
||||
>
|
||||
<Person />
|
||||
{onlineMembers}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-row place-content-center"
|
||||
title={`These are action points, you get ${userMessageLimit} every ${epochLengthSeconds} seconds`}
|
||||
|
||||
Reference in New Issue
Block a user