mirror of
https://github.com/Discreetly/frontend.git
synced 2026-01-08 04:23:56 -05:00
encrpyted rooms work
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -22,7 +22,7 @@
|
||||
"date-fns": "^2.30.0",
|
||||
"discreetly-interfaces": "^0.1.42",
|
||||
"dompurify": "^3.0.5",
|
||||
"idc-nullifier": "^0.0.6",
|
||||
"idc-nullifier": "^0.0.8",
|
||||
"libsodium-wrappers": "^0.7.11",
|
||||
"marked": "^7.0.5",
|
||||
"minidenticons": "^4.2.0",
|
||||
@@ -7209,9 +7209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/idc-nullifier": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/idc-nullifier/-/idc-nullifier-0.0.6.tgz",
|
||||
"integrity": "sha512-jM2D9ODNnmqn/Ps/cvpxSk4JTIrMG2Ozb0Z3a99jW5cd1DS7U5YBlHJRMRJn3sGyG3YIrIHyJQYoAWPBw5UiTA==",
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/idc-nullifier/-/idc-nullifier-0.0.8.tgz",
|
||||
"integrity": "sha512-fNR32XHu53AoAFA7rwVs2J67U/Za1hUpAPkIohwa05lnjXrwPMoA1zBeUutkdVRgmRBMbLpfAfXsUUnjqqpQQQ==",
|
||||
"dependencies": {
|
||||
"@semaphore-protocol/identity": "^3.10.1",
|
||||
"circomlib": "^2.0.5",
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"date-fns": "^2.30.0",
|
||||
"discreetly-interfaces": "^0.1.42",
|
||||
"dompurify": "^3.0.5",
|
||||
"idc-nullifier": "^0.0.6",
|
||||
"idc-nullifier": "^0.0.8",
|
||||
"libsodium-wrappers": "^0.7.11",
|
||||
"marked": "^7.0.5",
|
||||
"minidenticons": "^4.2.0",
|
||||
@@ -86,4 +86,4 @@
|
||||
"wagmi": "^1.4.3",
|
||||
"zuauth": "^0.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,6 @@
|
||||
loadingRooms = true;
|
||||
getEthAddressRoomNames($selectedServer, address)
|
||||
.then((groupNames) => {
|
||||
console.log(groupNames);
|
||||
loadingRooms = false;
|
||||
groups = groupNames;
|
||||
})
|
||||
@@ -84,8 +83,7 @@
|
||||
<h3 class="h4 mb-2"><span class="text-success-500">Step 1:</span> Connect your wallet</h3>
|
||||
<button
|
||||
bind:this={btnEl}
|
||||
class="btn variant-outline-tertiary">Connect</button
|
||||
>
|
||||
class="btn variant-outline-tertiary">Connect</button>
|
||||
</div>
|
||||
{#if isConnected}
|
||||
<div>
|
||||
@@ -106,8 +104,7 @@
|
||||
<button
|
||||
on:click={proveOwnership}
|
||||
id="btn"
|
||||
class="btn variant-outline-success">Sign</button
|
||||
>
|
||||
class="btn variant-outline-success">Sign</button>
|
||||
</div>
|
||||
{:else if loadingRooms}
|
||||
<p>Loading rooms...</p>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
console.warn(err);
|
||||
alertQueue.enqueue(`Unexpected error: ${err.message}`, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -90,8 +90,7 @@
|
||||
{#if !hideInput}
|
||||
<label
|
||||
class="label"
|
||||
for="inviteCode"
|
||||
>
|
||||
for="inviteCode">
|
||||
<span class="h5">Enter Invite Code:</span>
|
||||
<input
|
||||
class="input"
|
||||
@@ -99,8 +98,7 @@
|
||||
placeholder="Invite Code"
|
||||
id="inviteCode"
|
||||
bind:value={code}
|
||||
on:keydown={(event) => inviteCodeKeyPress(event)}
|
||||
/>
|
||||
on:keydown={(event) => inviteCodeKeyPress(event)} />
|
||||
</label>
|
||||
{/if}
|
||||
{#if !loading}
|
||||
@@ -108,8 +106,7 @@
|
||||
class="btn variant-ghost-success mt-3"
|
||||
type="button"
|
||||
disabled={!code}
|
||||
on:click={() => addCode(code)}>{buttonText}</button
|
||||
>
|
||||
on:click={() => addCode(code)}>{buttonText}</button>
|
||||
{:else}
|
||||
<div class="text-primary">Going Anon, please wait...</div>
|
||||
<Loading />
|
||||
@@ -120,8 +117,7 @@
|
||||
<div>
|
||||
If you are having trouble and would like help, please message us on <a
|
||||
href="https://discord.gg/brJQ36KVxk"
|
||||
class="underline link">Discord</a
|
||||
>
|
||||
class="underline link">Discord</a>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
console.warn(err);
|
||||
alertQueue.enqueue(`Unexpected error: ${err.message}`, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -55,9 +55,7 @@
|
||||
class="px-2"
|
||||
name="the-word-proof.json"
|
||||
bind:files
|
||||
on:change={onChangeHandler}
|
||||
/></label
|
||||
>
|
||||
on:change={onChangeHandler} /></label>
|
||||
</section>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
|
||||
@@ -132,7 +132,7 @@ async function genProof(
|
||||
);
|
||||
|
||||
const proof = prover.generateProof(proofInputs).then((proof: RLNFullProof) => {
|
||||
console.log('Proof generated!');
|
||||
console.info('Proof generated!');
|
||||
const msg: MessageI = {
|
||||
messageId: proof.snarkProof.publicSignals.nullifier.toString(),
|
||||
message: message,
|
||||
|
||||
@@ -128,3 +128,23 @@ export async function getAuth(
|
||||
throw new Error(`Failed to post to ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - makes a get request to the api
|
||||
* @param {string[] | string} urlParts - the url parts to be joined to form the url
|
||||
* @param {object} data - the data to be sent to the api
|
||||
* @returns {object} - the response from the api
|
||||
* @throws {Error} - if the request fails
|
||||
*/
|
||||
export async function postRaw(urlParts: string[] | string, data: object): Promise<Response> {
|
||||
const url = cleanURL(urlParts);
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { MessageI, ServerI } from 'discreetly-interfaces';
|
||||
import type { IdentityStoreI, Invites, JoinResponseI, JubmojiProofI, RoomI } from '$lib/types';
|
||||
import { Prover } from 'idc-nullifier';
|
||||
import type { Identity } from '@semaphore-protocol/identity';
|
||||
import { get, getAuth, post, postAuth } from './api';
|
||||
import { get, getAuth, post, postAuth, postRaw } from './api';
|
||||
import { getIdentity } from '$lib/utils';
|
||||
import { alertQueue } from '$lib/stores';
|
||||
|
||||
@@ -193,6 +193,22 @@ export async function postCheckRoomPassword(
|
||||
return Boolean(response.success);
|
||||
}
|
||||
|
||||
export async function postDoesRoomPasswordExist(
|
||||
serverUrl: string,
|
||||
roomId: string
|
||||
): Promise<boolean> {
|
||||
const response = await postRaw([serverUrl, `room/checkpasswordhash/${roomId}`], {
|
||||
passwordHash: 'passwordHash'
|
||||
});
|
||||
if (response.status == 200 && (response.json() as unknown as CheckResponse).success == false) {
|
||||
return false;
|
||||
} else if (response.status == 400) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function postSetRoomPassword(
|
||||
serverUrl: string,
|
||||
roomId: string,
|
||||
@@ -207,7 +223,6 @@ export async function postSetRoomPassword(
|
||||
idForProof.nullifier = id._nullifier;
|
||||
idForProof.secret = id._secret;
|
||||
idForProof.commitment = id._commitment;
|
||||
|
||||
// Proves you know the identity secret with a timestamp so this proof can't be replayed
|
||||
prover
|
||||
.generateProof({
|
||||
|
||||
@@ -65,7 +65,7 @@ export const pixelStore = sessionable({} as pixelStoreI, 'pixelmaps');
|
||||
/**
|
||||
* @description Stores the encrypted key for each room keyed by the roomId
|
||||
*/
|
||||
export const roomPassStore = encryptable({} as roomPassStoreI, 'roomKey');
|
||||
export const roomPassStore = encryptable({} as roomPassStoreI, 'roomPasses');
|
||||
|
||||
/**
|
||||
* @description Derived Store: The messages of the currently selected room
|
||||
|
||||
@@ -32,7 +32,7 @@ export function createIdentity(
|
||||
alertQueue.enqueue('Identity Created! Congrats on your new journey', 'success');
|
||||
return 'created';
|
||||
} else {
|
||||
console.log(get(identityExists));
|
||||
console.warn(get(identityExists));
|
||||
alertQueue.enqueue('Error creating identity!!!', 'error');
|
||||
return 'error';
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
identityExists,
|
||||
alertQueue,
|
||||
roomPassStore,
|
||||
selectedServer
|
||||
selectedServer,
|
||||
roomKeyStore
|
||||
} from '$lib/stores';
|
||||
import { encryptIdentity } from './identity';
|
||||
import type { IdentityStoreI } from '$lib/types';
|
||||
@@ -75,6 +76,17 @@ export async function unlockPadlock(password: string) {
|
||||
identityKeyStore.read();
|
||||
roomPassStore.read();
|
||||
updateRooms();
|
||||
console.debug('Unlocking rooms');
|
||||
setTimeout(() => {
|
||||
const roomPasses = get(roomPassStore);
|
||||
Object.keys(roomPasses).forEach(async (roomId) => {
|
||||
const roomKey = await deriveKey(roomPasses[roomId].password);
|
||||
roomKeyStore.update((roomKeys) => {
|
||||
roomKeys[roomId] = roomKey;
|
||||
return roomKeys;
|
||||
});
|
||||
});
|
||||
}, 50);
|
||||
});
|
||||
} else {
|
||||
alertQueue.enqueue('Incorrect Password', 'warning');
|
||||
@@ -85,12 +97,17 @@ export async function unlockPadlock(password: string) {
|
||||
export async function enterRoomPassword(password: string, roomId: string): Promise<boolean> {
|
||||
const hashedSaltedPassword = await hashPassword(password + roomId);
|
||||
if (hashedSaltedPassword) {
|
||||
postCheckRoomPassword(get(selectedServer), roomId, hashedSaltedPassword).then((res) => {
|
||||
postCheckRoomPassword(get(selectedServer), roomId, hashedSaltedPassword).then(async (res) => {
|
||||
if (res) {
|
||||
roomPassStore.update((roomPass) => {
|
||||
roomPass[roomId] = { password, hashedSaltedPassword };
|
||||
return roomPass;
|
||||
});
|
||||
const key = await deriveKey(password);
|
||||
roomKeyStore.update((roomKeys) => {
|
||||
roomKeys[roomId] = key;
|
||||
return roomKeys;
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -103,12 +120,17 @@ export async function enterRoomPassword(password: string, roomId: string): Promi
|
||||
export async function setRoomPassword(password: string, roomId: string): Promise<boolean> {
|
||||
const hashedSaltedPassword = await hashPassword(password + roomId);
|
||||
if (hashedSaltedPassword) {
|
||||
postSetRoomPassword(get(selectedServer), roomId, hashedSaltedPassword).then((res) => {
|
||||
postSetRoomPassword(get(selectedServer), roomId, hashedSaltedPassword).then(async (res) => {
|
||||
if (res) {
|
||||
roomPassStore.update((roomPass) => {
|
||||
roomPass[roomId] = { password, hashedSaltedPassword };
|
||||
return roomPass;
|
||||
});
|
||||
const key = await deriveKey(password);
|
||||
roomKeyStore.update((roomKeys) => {
|
||||
roomKeys[roomId] = key;
|
||||
return roomKeys;
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
|
||||
|
||||
onMount(async () => {
|
||||
console.log('Starting Up Application');
|
||||
console.info('Starting Up Application');
|
||||
if (getServerList().length === 0) {
|
||||
setDefaultServers();
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
drawerStore.close();
|
||||
}
|
||||
} else {
|
||||
console.log('Input field focused, not opening console');
|
||||
console.debug('Input field focused, not opening console');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<a
|
||||
class="btn variant-filled-primary"
|
||||
href="/admin/addAdmin">Add Room Admin</a>
|
||||
<a
|
||||
class="btn variant-filled-warning"
|
||||
href="/settings/debug">Debug</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
async function newCodes(numCodes: number) {
|
||||
console.log(`Requesting ${numCodes} codes`);
|
||||
console.debug(`Requesting ${numCodes} codes`);
|
||||
const canvasContainer = document.getElementById('qr');
|
||||
canvasContainer?.replaceChildren();
|
||||
if (usesLeft == 0) {
|
||||
@@ -44,7 +44,6 @@
|
||||
expiresAt,
|
||||
usesLeft
|
||||
);
|
||||
console.log(resp);
|
||||
const codes = resp.codes;
|
||||
codes.forEach((code) => {
|
||||
makeInviteQRCode(code.claimcode);
|
||||
@@ -68,14 +67,12 @@
|
||||
<div class="flex flex-col place-content-center max-w-sm m-auto pt-5">
|
||||
<div
|
||||
id="qr"
|
||||
class="flex flex-col gap-12 place-content-center"
|
||||
>
|
||||
class="flex flex-col gap-12 place-content-center">
|
||||
<div>
|
||||
<canvas
|
||||
class="variant-soft-secondary"
|
||||
width="250"
|
||||
height="250"
|
||||
/>
|
||||
height="250" />
|
||||
<p>no code generated yet</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,8 +80,7 @@
|
||||
class="btn my-12 variant-soft-primary text-center items-center"
|
||||
on:click={() => {
|
||||
newCodes(numCodes);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{#if numCodes > 1}
|
||||
Generate New Codes
|
||||
{:else}
|
||||
@@ -109,10 +105,8 @@
|
||||
type="range"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={numCodes}
|
||||
/>
|
||||
</label></svelte:fragment
|
||||
>
|
||||
bind:value={numCodes} />
|
||||
</label></svelte:fragment>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<svelte:fragment slot="summary">Number of Uses Per Code</svelte:fragment>
|
||||
@@ -123,10 +117,8 @@
|
||||
type="range"
|
||||
min="0"
|
||||
max="50"
|
||||
bind:value={usesLeft}
|
||||
/>
|
||||
</label></svelte:fragment
|
||||
>
|
||||
bind:value={usesLeft} />
|
||||
</label></svelte:fragment>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<svelte:fragment slot="summary">Expiration</svelte:fragment>
|
||||
@@ -137,10 +129,8 @@
|
||||
type="range"
|
||||
min="1"
|
||||
max="14"
|
||||
bind:value={daysFromNow}
|
||||
/>
|
||||
</label></svelte:fragment
|
||||
>
|
||||
bind:value={daysFromNow} />
|
||||
</label></svelte:fragment>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<svelte:fragment slot="summary">Rooms</svelte:fragment>
|
||||
@@ -152,13 +142,11 @@
|
||||
type="checkbox"
|
||||
value={room.roomId}
|
||||
on:change={updateRoomList}
|
||||
checked={roomIds.includes(String(room.roomId))}
|
||||
/>
|
||||
checked={roomIds.includes(String(room.roomId))} />
|
||||
<span title={String(room.roomId)}>{room.name}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div></svelte:fragment
|
||||
>
|
||||
</div></svelte:fragment>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<svelte:fragment slot="summary">API</svelte:fragment>
|
||||
@@ -168,18 +156,15 @@
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
bind:value={$configStore.apiUsername}
|
||||
/>
|
||||
bind:value={$configStore.apiUsername} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Api Password</span>
|
||||
<input
|
||||
type="password"
|
||||
class="input"
|
||||
bind:value={$configStore.apiPassword}
|
||||
/>
|
||||
</label></svelte:fragment
|
||||
>
|
||||
bind:value={$configStore.apiPassword} />
|
||||
</label></svelte:fragment>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
async function getRooms() {
|
||||
if (username && password) {
|
||||
console.log('getting rooms');
|
||||
console.debug('getting rooms');
|
||||
rooms = await getAllRooms($selectedServer, username, password);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
} else {
|
||||
selectedRoomIds = selectedRoomIds.filter((id) => id !== roomId);
|
||||
}
|
||||
console.log(selectedRoomIds);
|
||||
console.debug('selectedRoomIds', selectedRoomIds);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@
|
||||
formData.bandadaGroupId,
|
||||
formData.bandadaApiKey
|
||||
).then((res) => {
|
||||
console.log(res);
|
||||
console.debug(res);
|
||||
submitted = true;
|
||||
createdCodes = res.claimCodes;
|
||||
console.log(createdCodes);
|
||||
console.info(createdCodes);
|
||||
});
|
||||
} else {
|
||||
alertQueue.enqueue('You must create an identity before creating a room', 'error');
|
||||
@@ -62,8 +62,7 @@
|
||||
handleSubmit();
|
||||
}}
|
||||
buttonNext="variant-filled-surface-50-900-token"
|
||||
buttonComplete="variant-filled-success"
|
||||
>
|
||||
buttonComplete="variant-filled-success">
|
||||
<RoomName {formData} />
|
||||
<MembershipType {formData} />
|
||||
<RateLimit {formData} />
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
import * as emoji from 'node-emoji';
|
||||
|
||||
export let bubbleText: string;
|
||||
|
||||
let formattedText = '';
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
// Only render text on the frontend because DOMPurify
|
||||
// needs access to the DOM.
|
||||
formattedText = formatText(bubbleText);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
currentSelectedRoom,
|
||||
keyStore,
|
||||
rateLimitStore,
|
||||
roomKeyStore,
|
||||
alertQueue,
|
||||
identityExists,
|
||||
configStore
|
||||
@@ -12,13 +10,16 @@
|
||||
import type { Socket } from 'socket.io-client';
|
||||
import { getIdentity, clearMessageHistory } from '$lib/utils';
|
||||
import Send from 'svelte-material-icons/Send.svelte';
|
||||
import { decrypt, encrypt } from '$lib/crypto/crypto';
|
||||
import { encrypt } from '$lib/crypto/crypto';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let socket: Socket;
|
||||
export let connected: boolean;
|
||||
export let currentEpoch: number;
|
||||
export let userMessageLimit: number;
|
||||
export let roomId: string;
|
||||
export let getKey: () => Promise<CryptoKey>;
|
||||
let key: CryptoKey;
|
||||
|
||||
let scrollChatEvent = new CustomEvent('scrollChat', {
|
||||
detail: { behavior: 'smooth', delay: 20 }
|
||||
@@ -111,27 +112,12 @@
|
||||
}
|
||||
|
||||
// Helper function to handle encrypted room messages
|
||||
async function handleEncryptedMessage(
|
||||
messageText: string,
|
||||
roomId: string,
|
||||
keyStore: any,
|
||||
roomKeyStore: any
|
||||
): Promise<string> {
|
||||
if ($currentSelectedRoom.encrypted == 'AES' && !roomKeyStore[roomId]) {
|
||||
throw new Error('ROOM IS ENCRYPTED BUT NO PASSWORD WAS FOUND');
|
||||
}
|
||||
if (!keyStore) {
|
||||
throw new Error('NO KEYSTORE FOUND');
|
||||
}
|
||||
|
||||
const key = await decrypt(roomKeyStore[roomId], keyStore);
|
||||
if (!key) {
|
||||
throw new Error('NO KEY FOUND');
|
||||
}
|
||||
const encryptedMessage = await encrypt(messageText, key as unknown as CryptoKey);
|
||||
async function handleEncryptedMessage(messageText: string, roomId: string): Promise<string> {
|
||||
const encryptedMessage = await encrypt(messageText, key);
|
||||
if (encryptedMessage == null) {
|
||||
throw new Error('ENCRYPTION FAILED');
|
||||
} else {
|
||||
console.debug(encryptedMessage);
|
||||
return encryptedMessage;
|
||||
}
|
||||
}
|
||||
@@ -164,12 +150,7 @@
|
||||
let messageToSend: string = messageText;
|
||||
|
||||
if (room.encrypted === 'AES') {
|
||||
messageToSend = await handleEncryptedMessage(
|
||||
messageText,
|
||||
room.roomId!.toString(),
|
||||
$keyStore,
|
||||
$roomKeyStore
|
||||
);
|
||||
messageToSend = await handleEncryptedMessage(messageText, room.roomId!.toString());
|
||||
}
|
||||
|
||||
const msg = await genProof(
|
||||
@@ -215,6 +196,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getKey().then((k) => {
|
||||
key = k;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="border-t border-surface-500/30 p-2 md:p-4 !border-dashed">
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
selectedServer,
|
||||
configStore,
|
||||
currentRoomsStore,
|
||||
roomPasswordSet
|
||||
roomPasswordSet,
|
||||
identityExists,
|
||||
roomKeyStore,
|
||||
roomPassStore
|
||||
} from '$lib/stores';
|
||||
import RoomPassword from './RoomPassword.svelte';
|
||||
import { Experiences } from '$lib/types';
|
||||
@@ -19,6 +22,7 @@
|
||||
import Conversation from './Conversation.svelte';
|
||||
import Draw from './Draw.svelte';
|
||||
import InputPrompt from './ChatInputPrompt.svelte';
|
||||
import { deriveKey } from '$lib/crypto/crypto';
|
||||
|
||||
const toastStore = getToastStore();
|
||||
|
||||
@@ -59,6 +63,20 @@
|
||||
|
||||
$: updateRooms($selectedServer, [roomId]);
|
||||
|
||||
async function getKey(): Promise<CryptoKey> {
|
||||
let key: CryptoKey;
|
||||
if (!$roomPassStore[roomId]) {
|
||||
throw new Error('ROOM IS ENCRYPTED BUT NO PASSWORD WAS FOUND');
|
||||
}
|
||||
if (!$roomKeyStore[roomId]) {
|
||||
key = await deriveKey($roomPassStore[roomId].password);
|
||||
$roomKeyStore[roomId] = key;
|
||||
} else {
|
||||
key = $roomKeyStore[roomId];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function updateEpoch() {
|
||||
if ($currentSelectedRoom === undefined) {
|
||||
if ($currentRoomsStore[0] === undefined) {
|
||||
@@ -143,6 +161,7 @@
|
||||
updateEpoch();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeStore();
|
||||
socket.emit('leavingRoom', $currentSelectedRoom?.roomId);
|
||||
@@ -164,31 +183,39 @@
|
||||
{onlineMembers} />
|
||||
{#if $configStore.experience == Experiences.Chat}
|
||||
{#key $currentSelectedRoom.roomId}
|
||||
<Conversation {roomRateLimit} />
|
||||
<Conversation
|
||||
{roomRateLimit}
|
||||
{getKey} />
|
||||
{/key}
|
||||
<InputPrompt
|
||||
{socket}
|
||||
{connected}
|
||||
{currentEpoch}
|
||||
{userMessageLimit}
|
||||
{roomId} />
|
||||
{roomId}
|
||||
{getKey} />
|
||||
{:else if $configStore.experience == Experiences.Draw}
|
||||
<Draw />
|
||||
{:else}
|
||||
{#key $currentSelectedRoom.roomId}
|
||||
<Conversation {roomRateLimit} />
|
||||
<Conversation
|
||||
{roomRateLimit}
|
||||
{getKey} />
|
||||
{/key}
|
||||
<InputPrompt
|
||||
{socket}
|
||||
{connected}
|
||||
{currentEpoch}
|
||||
{userMessageLimit}
|
||||
{roomId} />
|
||||
{roomId}
|
||||
{getKey} />
|
||||
{/if}
|
||||
<!-- Conversation -->
|
||||
|
||||
<!-- Prompt -->
|
||||
</div>
|
||||
{:else if $identityExists == 'encrypted'}
|
||||
<h3 class="h3 my-5 text-center text-primary-500">Unlock your credentials to see this room.</h3>
|
||||
{:else}
|
||||
<div><RoomPassword {roomId} /></div>
|
||||
{/if}
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
import BubbleText from './BubbleText.svelte';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
import { bubbleBgFromSessionId } from '$lib/utils/color';
|
||||
import { decrypt } from '$lib/crypto/crypto';
|
||||
|
||||
export let roomRateLimit: number;
|
||||
export let getKey: () => Promise<CryptoKey>;
|
||||
let key: CryptoKey;
|
||||
|
||||
let elemChat: HTMLElement;
|
||||
|
||||
@@ -21,6 +24,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptText(text: string): Promise<string> {
|
||||
if (!key) {
|
||||
return getKey().then(async (k) => {
|
||||
key = k;
|
||||
const result = await decrypt(text, key);
|
||||
return result ? result : text;
|
||||
});
|
||||
} else if (key) {
|
||||
const result = await decrypt(text, key);
|
||||
return result ? result : text;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollChatBottom(behavior: ScrollBehavior = 'smooth', delay = 20, count = 0): void {
|
||||
if (count > 10) {
|
||||
console.warn('scrollChatBottom: elemChat is not defined after multiple attempts, giving up');
|
||||
@@ -56,6 +74,9 @@
|
||||
const delay = customEvent.detail.delay ? customEvent.detail.delay : 20;
|
||||
scrollChatBottom(behavior, delay);
|
||||
});
|
||||
getKey().then((k) => {
|
||||
key = k;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -78,7 +99,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#key msg}
|
||||
<BubbleText bubbleText={String(msg.message)} />
|
||||
{#await decryptText(String(msg.message))}
|
||||
<p>Decrypting...</p>
|
||||
{:then decryptedText}
|
||||
<BubbleText bubbleText={decryptedText} />
|
||||
{:catch error}
|
||||
<BubbleText bubbleText={String(msg.message)} />
|
||||
<!-- You can customize this error state -->
|
||||
{/await}
|
||||
{/key}
|
||||
</div>
|
||||
<footer class="flex justify-between items-center text-xs md:text-sm pb-1">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const parent = canvas.parentElement;
|
||||
const parentWidth = parent!.clientWidth;
|
||||
const parentHeight = parent!.clientHeight - 150;
|
||||
console.log(parentWidth, parentHeight);
|
||||
console.debug(parentWidth, parentHeight);
|
||||
let height, width;
|
||||
|
||||
if (parentHeight < parentWidth / aspectRatio) {
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
console.log(canvas.height, canvas.width);
|
||||
console.debug(canvas.height, canvas.width);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -53,5 +53,4 @@
|
||||
id="drawing-canvas"
|
||||
width="250"
|
||||
height="250"
|
||||
class="border border-surface-300-600-token m-1 sm:m-3"
|
||||
/>
|
||||
class="border border-surface-300-600-token m-1 sm:m-3" />
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
|
||||
$: if (room.encrypted) {
|
||||
if ($roomPassStore[room.roomId.toString()] && $keyStore) {
|
||||
console.log('Room is encrypted and encrypted password is stored');
|
||||
decrypt($roomPassStore[room.roomId.toString()], $keyStore);
|
||||
console.debug('Room is encrypted and encrypted password is stored');
|
||||
decrypt($roomPassStore[room.roomId.toString()].password, $keyStore);
|
||||
} else {
|
||||
console.log('Room is encrypted and password is not stored');
|
||||
console.debug('Room is encrypted and password is not stored');
|
||||
}
|
||||
} else {
|
||||
console.log('Room is not encrypted');
|
||||
console.debug('Room is not encrypted');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,35 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { enterRoomPassword } from '$lib/utils';
|
||||
import { enterRoomPassword, getCommitment } from '$lib/utils';
|
||||
import Container from '$lib/components/Utils/Container.svelte';
|
||||
import { postDoesRoomPasswordExist, postSetRoomPassword } from '$lib/services/server';
|
||||
import { roomPassStore, selectedServer, roomsStore } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { hashPassword } from '$lib/crypto/crypto';
|
||||
import Loading from '$lib/components/Utils/Loading.svelte';
|
||||
|
||||
export let roomId: string;
|
||||
|
||||
const minPasswordLength = 4;
|
||||
let _passwordSet: boolean;
|
||||
let r = '';
|
||||
let admin = false;
|
||||
let loaded = false;
|
||||
|
||||
function onSubmit() {
|
||||
if (r != '' && r != null && r != undefined && r.length >= minPasswordLength) {
|
||||
enterRoomPassword(r, roomId);
|
||||
function enterPassword() {
|
||||
enterRoomPassword(r, roomId);
|
||||
}
|
||||
|
||||
async function setRoomPassword() {
|
||||
const hashedSaltedPassword = await hashPassword(r + roomId);
|
||||
if (hashedSaltedPassword) {
|
||||
postSetRoomPassword($selectedServer, roomId, hashedSaltedPassword).then((answer) => {
|
||||
if (answer) {
|
||||
roomPassStore.update((roomPass) => {
|
||||
roomPass[roomId] = { password: r, hashedSaltedPassword };
|
||||
return roomPass;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to hash password');
|
||||
}
|
||||
}
|
||||
|
||||
function isPasswordSet() {
|
||||
try {
|
||||
postDoesRoomPasswordExist($selectedServer, roomId).then((answer) => {
|
||||
_passwordSet = answer;
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
const idc = getCommitment();
|
||||
$roomsStore[roomId].adminIdentities?.forEach((id) => {
|
||||
if (id === idc) {
|
||||
admin = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
isPasswordSet();
|
||||
isAdmin();
|
||||
loaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container heading="Enter Room Password">
|
||||
<form
|
||||
on:submit|preventDefault={() => onSubmit()}
|
||||
class="flex flex-col w-full">
|
||||
<label
|
||||
for="enterRoomPassword"
|
||||
class="label" />
|
||||
<input
|
||||
id="enterRoomPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
class="input"
|
||||
bind:value={r}
|
||||
required />
|
||||
<button
|
||||
class="btn variant-filled-primary mt-3"
|
||||
type="submit">Enter Room Password</button>
|
||||
</form>
|
||||
</Container>
|
||||
{#if loaded}
|
||||
{#if _passwordSet}
|
||||
<Container heading="Enter Room Password">
|
||||
<form
|
||||
on:submit|preventDefault={() => enterPassword()}
|
||||
class="flex flex-col w-full">
|
||||
<label
|
||||
for="enterRoomPassword"
|
||||
class="label" />
|
||||
<input
|
||||
id="enterRoomPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
class="input"
|
||||
bind:value={r}
|
||||
required />
|
||||
<button
|
||||
class="btn variant-filled-primary mt-3"
|
||||
type="submit">Enter Room Password</button>
|
||||
</form>
|
||||
</Container>
|
||||
{:else if admin}
|
||||
<Container heading="Set Room Password">
|
||||
<form
|
||||
on:submit|preventDefault={() => setRoomPassword()}
|
||||
class="flex flex-col w-full">
|
||||
<label
|
||||
for="setRoomPassword"
|
||||
class="label" />
|
||||
<input
|
||||
id="setRoomPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
class="input"
|
||||
bind:value={r}
|
||||
required />
|
||||
<button
|
||||
class="btn variant-filled-primary mt-3"
|
||||
type="submit">Set Room Password</button>
|
||||
</form>
|
||||
</Container>
|
||||
{:else}
|
||||
<Container heading="Password Not Set">
|
||||
<p class="text-center">
|
||||
The room you are trying to enter requires a password, but the password has not been set yet.
|
||||
Annoy the administrators who created this room!
|
||||
</p>
|
||||
</Container>
|
||||
{/if}
|
||||
{:else}
|
||||
<Loading />
|
||||
{/if}
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
addConsoleMessage(err, 'error');
|
||||
});
|
||||
break;
|
||||
@@ -161,5 +160,4 @@
|
||||
class="input py-1 px-2"
|
||||
type="text"
|
||||
{placeholder}
|
||||
on:keypress={handleInput}
|
||||
/>
|
||||
on:keypress={handleInput} />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
identityExists,
|
||||
identityKeyStore,
|
||||
identityStore,
|
||||
roomPassStore,
|
||||
lockStateStore,
|
||||
passwordSet,
|
||||
roomsStore,
|
||||
@@ -49,8 +50,7 @@
|
||||
<div class="p-4">
|
||||
<div
|
||||
id="status"
|
||||
class="flex flex-col gap-5"
|
||||
>
|
||||
class="flex flex-col gap-5">
|
||||
<div>
|
||||
<h2 class="h3">configStore</h2>
|
||||
<div>Completed Signup: {JSON.stringify($configStore.signUpStatus.completedSignup)}</div>
|
||||
@@ -103,19 +103,20 @@
|
||||
<div class="ms-4">membershipType: {membershipType}</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
roomPassStore: {JSON.stringify($roomPassStore)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t py-2 my-5">
|
||||
<button
|
||||
class="btn variant-outline-primary m-4"
|
||||
on:click={triggerAlert}>Test Alert</button
|
||||
>
|
||||
on:click={triggerAlert}>Test Alert</button>
|
||||
<select
|
||||
class="select"
|
||||
name="alertType"
|
||||
id="alertType"
|
||||
bind:value={t}
|
||||
>
|
||||
bind:value={t}>
|
||||
{#each c as choice}
|
||||
<option value={choice}>{choice}</option>
|
||||
{/each}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
$configStore.signUpStatus.inviteCode = $page.params.invite;
|
||||
console.log(`Invited with code: ${$page.params.invite}`);
|
||||
console.info(`Invited with code: ${$page.params.invite}`);
|
||||
|
||||
onMount(() => {
|
||||
if ($identityExists) {
|
||||
|
||||
@@ -4,13 +4,13 @@ Circuit_Type = "idcNullifier"
|
||||
[Circuit_Build]
|
||||
Circom_Version = "circom compiler 2.1.5"
|
||||
GitHub_URL = "https://github.com/Discreetly/IdentityCommitmentNullifierCircuit.git"
|
||||
Git_Commit = "b940865"
|
||||
Compilation_Time = 1698174365
|
||||
Git_Commit = "a0d6415"
|
||||
Compilation_Time = 1699876330
|
||||
|
||||
[Files]
|
||||
Wasm = "circuit.wasm"
|
||||
Wasm_SHA256SUM = "410da93349f51f96dfb1bbb2d6578617a3de2494cf46d1ca6c1910a0098c82b8"
|
||||
Zkey = "final.zkey"
|
||||
Zkey_SHA256SUM = "7a626d709b4f6dc1150b554b3e643bd22099eaad1b5ed8d3b0202a8756004248"
|
||||
Zkey_SHA256SUM = "6a375f8c6066abd38535bcadfc03c0616bdd80c2fca96bb6c1e4d4aa41f9662b"
|
||||
Verification_Key = "verification_key.json"
|
||||
Verification_Key_SHA256SUM = "1406f9e05c4bbbef93c9053cf0fa10a47881c5e8e3f1ada67e1ef3c5260fce91"
|
||||
Verification_Key_SHA256SUM = "cc7b95eb8adedbdbfe0ff150bca89597d888230a9c11901b7d0a81ad92b9b36d"
|
||||
|
||||
Binary file not shown.
@@ -1,95 +1,104 @@
|
||||
{
|
||||
"protocol": "groth16",
|
||||
"curve": "bn128",
|
||||
"nPublic": 3,
|
||||
"vk_alpha_1": [
|
||||
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
|
||||
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
|
||||
"1"
|
||||
],
|
||||
"vk_beta_2": [
|
||||
[
|
||||
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
|
||||
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
|
||||
],
|
||||
[
|
||||
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
|
||||
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
|
||||
],
|
||||
["1", "0"]
|
||||
],
|
||||
"vk_gamma_2": [
|
||||
[
|
||||
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
|
||||
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
|
||||
],
|
||||
[
|
||||
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
|
||||
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
|
||||
],
|
||||
["1", "0"]
|
||||
],
|
||||
"vk_delta_2": [
|
||||
[
|
||||
"17133313670231667487736004097020484843226717741134543071681595192213542404569",
|
||||
"18673817425770621231905673683883346893405144776326422608131560048409738195792"
|
||||
],
|
||||
[
|
||||
"11821370524809748218863201748748705728394144148925669901234409589641087093424",
|
||||
"18513628196399124885663344181867643312766015533146283344455531540544551221665"
|
||||
],
|
||||
["1", "0"]
|
||||
],
|
||||
"vk_alphabeta_12": [
|
||||
[
|
||||
[
|
||||
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
|
||||
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
|
||||
],
|
||||
[
|
||||
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
|
||||
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
|
||||
],
|
||||
[
|
||||
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
|
||||
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
|
||||
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
|
||||
],
|
||||
[
|
||||
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
|
||||
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
|
||||
],
|
||||
[
|
||||
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
|
||||
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
|
||||
]
|
||||
]
|
||||
],
|
||||
"IC": [
|
||||
[
|
||||
"10540353245037691655240058832465935071147222378795512774896416883563614389198",
|
||||
"20042286059857913285646233979306226083538103143380748863555356103197961213823",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"5831260214145524146501320942030802135386415386731869532840770699430145685903",
|
||||
"10936406497477698856797103676487112559914492317404578945377836522823774186039",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"1295407852409244811850984901965423450287105807829809873962419792828349329577",
|
||||
"11301654800484264796111478656697951131719015244071014736702281514571505462297",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"6823739927950557742246913693052072220213742766782370219666937126411380340633",
|
||||
"19878777316727584245579797737044924949420447706770682437221414501064053790692",
|
||||
"1"
|
||||
]
|
||||
]
|
||||
}
|
||||
"protocol": "groth16",
|
||||
"curve": "bn128",
|
||||
"nPublic": 3,
|
||||
"vk_alpha_1": [
|
||||
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
|
||||
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
|
||||
"1"
|
||||
],
|
||||
"vk_beta_2": [
|
||||
[
|
||||
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
|
||||
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
|
||||
],
|
||||
[
|
||||
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
|
||||
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_gamma_2": [
|
||||
[
|
||||
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
|
||||
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
|
||||
],
|
||||
[
|
||||
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
|
||||
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_delta_2": [
|
||||
[
|
||||
"2263054764437434763570286704031806516721917814051603656052135584430600195849",
|
||||
"332267239743604505067953832758848322829862699821984128892005653857113955614"
|
||||
],
|
||||
[
|
||||
"8107422899773313798194038382837705724842762783652811880556511721860229797648",
|
||||
"5609865441509880240754758422778809019695266789051834830754116541635728659997"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_alphabeta_12": [
|
||||
[
|
||||
[
|
||||
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
|
||||
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
|
||||
],
|
||||
[
|
||||
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
|
||||
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
|
||||
],
|
||||
[
|
||||
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
|
||||
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
|
||||
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
|
||||
],
|
||||
[
|
||||
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
|
||||
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
|
||||
],
|
||||
[
|
||||
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
|
||||
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
|
||||
]
|
||||
]
|
||||
],
|
||||
"IC": [
|
||||
[
|
||||
"10540353245037691655240058832465935071147222378795512774896416883563614389198",
|
||||
"20042286059857913285646233979306226083538103143380748863555356103197961213823",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"5831260214145524146501320942030802135386415386731869532840770699430145685903",
|
||||
"10936406497477698856797103676487112559914492317404578945377836522823774186039",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"1295407852409244811850984901965423450287105807829809873962419792828349329577",
|
||||
"11301654800484264796111478656697951131719015244071014736702281514571505462297",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"6823739927950557742246913693052072220213742766782370219666937126411380340633",
|
||||
"19878777316727584245579797737044924949420447706770682437221414501064053790692",
|
||||
"1"
|
||||
]
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user