mirror of
https://github.com/waku-org/js-waku.git
synced 2025-01-13 22:15:04 +00:00
Merge #773
773: Remove circular dependencies r=D4nte a=D4nte Co-authored-by: Franck Royer <franck@status.im>
This commit is contained in:
commit
9b6fd5ec8b
@ -1,3 +1,5 @@
|
|||||||
|
export { DefaultPubSubTopic } from "./lib/constants";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
generatePrivateKey,
|
generatePrivateKey,
|
||||||
generateSymmetricKey,
|
generateSymmetricKey,
|
||||||
@ -12,7 +14,7 @@ export * as enr from "./lib/enr";
|
|||||||
export * as utils from "./lib/utils";
|
export * as utils from "./lib/utils";
|
||||||
|
|
||||||
export * as waku from "./lib/waku";
|
export * as waku from "./lib/waku";
|
||||||
export { Waku, DefaultPubSubTopic, Protocols } from "./lib/waku";
|
export { Waku, Protocols } from "./lib/waku";
|
||||||
|
|
||||||
export * as waku_message from "./lib/waku_message";
|
export * as waku_message from "./lib/waku_message";
|
||||||
export { WakuMessage } from "./lib/waku_message";
|
export { WakuMessage } from "./lib/waku_message";
|
||||||
@ -25,7 +27,7 @@ export {
|
|||||||
} from "./lib/waku_light_push";
|
} from "./lib/waku_light_push";
|
||||||
|
|
||||||
export * as waku_relay from "./lib/waku_relay";
|
export * as waku_relay from "./lib/waku_relay";
|
||||||
export { WakuRelay, RelayCodecs } from "./lib/waku_relay";
|
export { WakuRelay } from "./lib/waku_relay";
|
||||||
|
|
||||||
export * as waku_store from "./lib/waku_store";
|
export * as waku_store from "./lib/waku_store";
|
||||||
export { PageDirection, WakuStore, StoreCodecs } from "./lib/waku_store";
|
export { PageDirection, WakuStore, StoreCodecs } from "./lib/waku_store";
|
||||||
|
9
src/lib/constants.ts
Normal file
9
src/lib/constants.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* DefaultPubSubTopic is the default gossipsub topic to use for Waku.
|
||||||
|
*/
|
||||||
|
export const DefaultPubSubTopic = "/waku/2/default-waku/proto";
|
||||||
|
|
||||||
|
export enum StoreCodecs {
|
||||||
|
V2Beta3 = "/vac/waku/store/2.0.0-beta3",
|
||||||
|
V2Beta4 = "/vac/waku/store/2.0.0-beta4",
|
||||||
|
}
|
@ -4,8 +4,7 @@ import * as secp from "@noble/secp256k1";
|
|||||||
import * as sha3 from "js-sha3";
|
import * as sha3 from "js-sha3";
|
||||||
import { concat } from "uint8arrays/concat";
|
import { concat } from "uint8arrays/concat";
|
||||||
|
|
||||||
import * as symmetric from "./waku_message/symmetric";
|
import { Asymmetric, Symmetric } from "./waku_message/constants";
|
||||||
import { PrivateKeySize } from "./waku_message/version_1";
|
|
||||||
|
|
||||||
declare const self: Record<string, any> | undefined;
|
declare const self: Record<string, any> | undefined;
|
||||||
const crypto: { node?: any; web?: any } = {
|
const crypto: { node?: any; web?: any } = {
|
||||||
@ -34,14 +33,14 @@ export const sha256 = secp.utils.sha256;
|
|||||||
* Use {@link getPublicKey} to get the corresponding Public Key.
|
* Use {@link getPublicKey} to get the corresponding Public Key.
|
||||||
*/
|
*/
|
||||||
export function generatePrivateKey(): Uint8Array {
|
export function generatePrivateKey(): Uint8Array {
|
||||||
return randomBytes(PrivateKeySize);
|
return randomBytes(Asymmetric.keySize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new symmetric key to be used for symmetric encryption.
|
* Generate a new symmetric key to be used for symmetric encryption.
|
||||||
*/
|
*/
|
||||||
export function generateSymmetricKey(): Uint8Array {
|
export function generateSymmetricKey(): Uint8Array {
|
||||||
return randomBytes(symmetric.KeySize);
|
return randomBytes(Symmetric.keySize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,8 +2,8 @@ import debug from "debug";
|
|||||||
import { Multiaddr } from "multiaddr";
|
import { Multiaddr } from "multiaddr";
|
||||||
|
|
||||||
import { DnsNodeDiscovery, NodeCapabilityCount } from "./dns";
|
import { DnsNodeDiscovery, NodeCapabilityCount } from "./dns";
|
||||||
|
import { getPredefinedBootstrapNodes } from "./predefined";
|
||||||
import { getPredefinedBootstrapNodes, getPseudoRandomSubset } from "./index";
|
import { getPseudoRandomSubset } from "./random_subset";
|
||||||
|
|
||||||
const dbg = debug("waku:discovery:bootstrap");
|
const dbg = debug("waku:discovery:bootstrap");
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
|
||||||
import { fleets } from "./predefined";
|
import { fleets } from "./predefined";
|
||||||
|
import { getPseudoRandomSubset } from "./random_subset";
|
||||||
import { getPseudoRandomSubset } from "./index";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -1,19 +1,6 @@
|
|||||||
import { shuffle } from "libp2p-gossipsub/src/utils";
|
|
||||||
|
|
||||||
export { getPredefinedBootstrapNodes } from "./predefined";
|
export { getPredefinedBootstrapNodes } from "./predefined";
|
||||||
export * as predefined from "./predefined";
|
export * as predefined from "./predefined";
|
||||||
export { Bootstrap, BootstrapOptions } from "./bootstrap";
|
export { Bootstrap, BootstrapOptions } from "./bootstrap";
|
||||||
export * as dns from "./dns";
|
export * as dns from "./dns";
|
||||||
export { Endpoints, DnsOverHttps } from "./dns_over_https";
|
export { Endpoints, DnsOverHttps } from "./dns_over_https";
|
||||||
export { ENRTree, ENRTreeValues, ENRRootValues } from "./enrtree";
|
export { ENRTree, ENRTreeValues, ENRRootValues } from "./enrtree";
|
||||||
|
|
||||||
export function getPseudoRandomSubset<T>(
|
|
||||||
values: T[],
|
|
||||||
wantedNumber: number
|
|
||||||
): T[] {
|
|
||||||
if (values.length <= wantedNumber) {
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
return shuffle(values).slice(0, wantedNumber);
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Multiaddr } from "multiaddr";
|
import { Multiaddr } from "multiaddr";
|
||||||
|
|
||||||
import { getPseudoRandomSubset } from "./index";
|
import { getPseudoRandomSubset } from "./random_subset";
|
||||||
|
|
||||||
export const DefaultWantedNumber = 1;
|
export const DefaultWantedNumber = 1;
|
||||||
|
|
||||||
|
12
src/lib/discovery/random_subset.ts
Normal file
12
src/lib/discovery/random_subset.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { shuffle } from "libp2p-gossipsub/src/utils/index";
|
||||||
|
|
||||||
|
export function getPseudoRandomSubset<T>(
|
||||||
|
values: T[],
|
||||||
|
wantedNumber: number
|
||||||
|
): T[] {
|
||||||
|
if (values.length <= wantedNumber) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shuffle(values).slice(0, wantedNumber);
|
||||||
|
}
|
@ -20,8 +20,8 @@ import { Bootstrap, BootstrapOptions } from "./discovery";
|
|||||||
import { FilterCodec, WakuFilter } from "./waku_filter";
|
import { FilterCodec, WakuFilter } from "./waku_filter";
|
||||||
import { LightPushCodec, WakuLightPush } from "./waku_light_push";
|
import { LightPushCodec, WakuLightPush } from "./waku_light_push";
|
||||||
import { DecryptionMethod, WakuMessage } from "./waku_message";
|
import { DecryptionMethod, WakuMessage } from "./waku_message";
|
||||||
import { RelayCodecs, WakuRelay } from "./waku_relay";
|
import { WakuRelay } from "./waku_relay";
|
||||||
import { RelayPingContentTopic } from "./waku_relay/constants";
|
import { RelayCodecs, RelayPingContentTopic } from "./waku_relay/constants";
|
||||||
import { StoreCodecs, WakuStore } from "./waku_store";
|
import { StoreCodecs, WakuStore } from "./waku_store";
|
||||||
|
|
||||||
const websocketsTransportKey = Websockets.prototype[Symbol.toStringTag];
|
const websocketsTransportKey = Websockets.prototype[Symbol.toStringTag];
|
||||||
@ -29,11 +29,6 @@ const websocketsTransportKey = Websockets.prototype[Symbol.toStringTag];
|
|||||||
export const DefaultPingKeepAliveValueSecs = 0;
|
export const DefaultPingKeepAliveValueSecs = 0;
|
||||||
export const DefaultRelayKeepAliveValueSecs = 5 * 60;
|
export const DefaultRelayKeepAliveValueSecs = 5 * 60;
|
||||||
|
|
||||||
/**
|
|
||||||
* DefaultPubSubTopic is the default gossipsub topic to use for Waku.
|
|
||||||
*/
|
|
||||||
export const DefaultPubSubTopic = "/waku/2/default-waku/proto";
|
|
||||||
|
|
||||||
const dbg = debug("waku:waku");
|
const dbg = debug("waku:waku");
|
||||||
|
|
||||||
export enum Protocols {
|
export enum Protocols {
|
||||||
|
@ -5,9 +5,9 @@ import Libp2p, { MuxedStream } from "libp2p";
|
|||||||
import { Peer, PeerId } from "libp2p/src/peer-store";
|
import { Peer, PeerId } from "libp2p/src/peer-store";
|
||||||
|
|
||||||
import { WakuMessage as WakuMessageProto } from "../../proto/waku/v2/message";
|
import { WakuMessage as WakuMessageProto } from "../../proto/waku/v2/message";
|
||||||
|
import { DefaultPubSubTopic } from "../constants";
|
||||||
import { getPeersForProtocol, selectRandomPeer } from "../select_peer";
|
import { getPeersForProtocol, selectRandomPeer } from "../select_peer";
|
||||||
import { hexToBytes } from "../utils";
|
import { hexToBytes } from "../utils";
|
||||||
import { DefaultPubSubTopic } from "../waku";
|
|
||||||
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
||||||
|
|
||||||
import { ContentFilter, FilterRPC } from "./filter_rpc";
|
import { ContentFilter, FilterRPC } from "./filter_rpc";
|
||||||
|
@ -6,8 +6,8 @@ import { Peer } from "libp2p/src/peer-store";
|
|||||||
import PeerId from "peer-id";
|
import PeerId from "peer-id";
|
||||||
|
|
||||||
import { PushResponse } from "../../proto/waku/v2/light_push";
|
import { PushResponse } from "../../proto/waku/v2/light_push";
|
||||||
|
import { DefaultPubSubTopic } from "../constants";
|
||||||
import { getPeersForProtocol, selectRandomPeer } from "../select_peer";
|
import { getPeersForProtocol, selectRandomPeer } from "../select_peer";
|
||||||
import { DefaultPubSubTopic } from "../waku";
|
|
||||||
import { WakuMessage } from "../waku_message";
|
import { WakuMessage } from "../waku_message";
|
||||||
|
|
||||||
import { PushRPC } from "./push_rpc";
|
import { PushRPC } from "./push_rpc";
|
||||||
|
10
src/lib/waku_message/constants.ts
Normal file
10
src/lib/waku_message/constants.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const Symmetric = {
|
||||||
|
keySize: 32,
|
||||||
|
ivSize: 12,
|
||||||
|
tagSize: 16,
|
||||||
|
algorithm: { name: "AES-GCM", length: 128 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Asymmetric = {
|
||||||
|
keySize: 32,
|
||||||
|
};
|
@ -1,10 +1,6 @@
|
|||||||
import { getSubtle, randomBytes } from "../crypto";
|
import { getSubtle, randomBytes } from "../crypto";
|
||||||
|
|
||||||
export const KeySize = 32;
|
import { Symmetric } from "./constants";
|
||||||
export const IvSize = 12;
|
|
||||||
export const TagSize = 16;
|
|
||||||
|
|
||||||
const Algorithm = { name: "AES-GCM", length: 128 };
|
|
||||||
|
|
||||||
export async function encrypt(
|
export async function encrypt(
|
||||||
iv: Uint8Array,
|
iv: Uint8Array,
|
||||||
@ -12,9 +8,9 @@ export async function encrypt(
|
|||||||
clearText: Uint8Array
|
clearText: Uint8Array
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
return getSubtle()
|
return getSubtle()
|
||||||
.importKey("raw", key, Algorithm, false, ["encrypt"])
|
.importKey("raw", key, Symmetric.algorithm, false, ["encrypt"])
|
||||||
.then((cryptoKey) =>
|
.then((cryptoKey) =>
|
||||||
getSubtle().encrypt({ iv, ...Algorithm }, cryptoKey, clearText)
|
getSubtle().encrypt({ iv, ...Symmetric.algorithm }, cryptoKey, clearText)
|
||||||
)
|
)
|
||||||
.then((cipher) => new Uint8Array(cipher));
|
.then((cipher) => new Uint8Array(cipher));
|
||||||
}
|
}
|
||||||
@ -25,13 +21,13 @@ export async function decrypt(
|
|||||||
cipherText: Uint8Array
|
cipherText: Uint8Array
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
return getSubtle()
|
return getSubtle()
|
||||||
.importKey("raw", key, Algorithm, false, ["decrypt"])
|
.importKey("raw", key, Symmetric.algorithm, false, ["decrypt"])
|
||||||
.then((cryptoKey) =>
|
.then((cryptoKey) =>
|
||||||
getSubtle().decrypt({ iv, ...Algorithm }, cryptoKey, cipherText)
|
getSubtle().decrypt({ iv, ...Symmetric.algorithm }, cryptoKey, cipherText)
|
||||||
)
|
)
|
||||||
.then((clear) => new Uint8Array(clear));
|
.then((clear) => new Uint8Array(clear));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateIv(): Uint8Array {
|
export function generateIv(): Uint8Array {
|
||||||
return randomBytes(IvSize);
|
return randomBytes(Symmetric.ivSize);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { concat } from "uint8arrays/concat";
|
|||||||
import { keccak256, randomBytes, sign } from "../crypto";
|
import { keccak256, randomBytes, sign } from "../crypto";
|
||||||
import { hexToBytes } from "../utils";
|
import { hexToBytes } from "../utils";
|
||||||
|
|
||||||
|
import { Symmetric } from "./constants";
|
||||||
import * as ecies from "./ecies";
|
import * as ecies from "./ecies";
|
||||||
import * as symmetric from "./symmetric";
|
import * as symmetric from "./symmetric";
|
||||||
|
|
||||||
@ -13,8 +14,6 @@ const IsSignedMask = 4; // 0100
|
|||||||
const PaddingTarget = 256;
|
const PaddingTarget = 256;
|
||||||
const SignatureLength = 65;
|
const SignatureLength = 65;
|
||||||
|
|
||||||
export const PrivateKeySize = 32;
|
|
||||||
|
|
||||||
export type Signature = {
|
export type Signature = {
|
||||||
signature: Uint8Array;
|
signature: Uint8Array;
|
||||||
publicKey: Uint8Array | undefined;
|
publicKey: Uint8Array | undefined;
|
||||||
@ -187,7 +186,7 @@ export async function decryptSymmetric(
|
|||||||
payload: Uint8Array,
|
payload: Uint8Array,
|
||||||
key: Uint8Array | string
|
key: Uint8Array | string
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const ivStart = payload.length - symmetric.IvSize;
|
const ivStart = payload.length - Symmetric.ivSize;
|
||||||
const cipher = payload.slice(0, ivStart);
|
const cipher = payload.slice(0, ivStart);
|
||||||
const iv = payload.slice(ivStart);
|
const iv = payload.slice(ivStart);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Gossipsub from "libp2p-gossipsub";
|
import Gossipsub from "libp2p-gossipsub";
|
||||||
import { shuffle } from "libp2p-gossipsub/src/utils";
|
import { shuffle } from "libp2p-gossipsub/src/utils";
|
||||||
|
|
||||||
import { RelayCodecs } from "./index";
|
import { RelayCodecs } from "./constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a topic, returns up to count peers subscribed to that topic
|
* Given a topic, returns up to count peers subscribed to that topic
|
||||||
|
@ -8,12 +8,13 @@ import {
|
|||||||
Nwaku,
|
Nwaku,
|
||||||
} from "../../test_utils";
|
} from "../../test_utils";
|
||||||
import { delay } from "../../test_utils/delay";
|
import { delay } from "../../test_utils/delay";
|
||||||
|
import { DefaultPubSubTopic } from "../constants";
|
||||||
import {
|
import {
|
||||||
generatePrivateKey,
|
generatePrivateKey,
|
||||||
generateSymmetricKey,
|
generateSymmetricKey,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
} from "../crypto";
|
} from "../crypto";
|
||||||
import { DefaultPubSubTopic, Protocols, Waku } from "../waku";
|
import { Protocols, Waku } from "../waku";
|
||||||
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
||||||
|
|
||||||
const log = debug("waku:test");
|
const log = debug("waku:test");
|
||||||
|
@ -13,19 +13,17 @@ import { InMessage } from "libp2p-interfaces/src/pubsub";
|
|||||||
import { SignaturePolicy } from "libp2p-interfaces/src/pubsub/signature-policy";
|
import { SignaturePolicy } from "libp2p-interfaces/src/pubsub/signature-policy";
|
||||||
import PeerId from "peer-id";
|
import PeerId from "peer-id";
|
||||||
|
|
||||||
|
import { DefaultPubSubTopic } from "../constants";
|
||||||
import { hexToBytes } from "../utils";
|
import { hexToBytes } from "../utils";
|
||||||
import { CreateOptions, DefaultPubSubTopic } from "../waku";
|
import { CreateOptions } from "../waku";
|
||||||
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
||||||
|
|
||||||
import * as constants from "./constants";
|
import * as constants from "./constants";
|
||||||
import { RelayCodecs } from "./constants";
|
|
||||||
import { getRelayPeers } from "./get_relay_peers";
|
import { getRelayPeers } from "./get_relay_peers";
|
||||||
import { RelayHeartbeat } from "./relay_heartbeat";
|
import { RelayHeartbeat } from "./relay_heartbeat";
|
||||||
|
|
||||||
const dbg = debug("waku:relay");
|
const dbg = debug("waku:relay");
|
||||||
|
|
||||||
export { RelayCodecs };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See constructor libp2p-gossipsub [API](https://github.com/ChainSafe/js-libp2p-gossipsub#api).
|
* See constructor libp2p-gossipsub [API](https://github.com/ChainSafe/js-libp2p-gossipsub#api).
|
||||||
*/
|
*/
|
||||||
|
4
src/lib/waku_store/constants.ts
Normal file
4
src/lib/waku_store/constants.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum StoreCodecs {
|
||||||
|
V2Beta3 = "/vac/waku/store/2.0.0-beta3",
|
||||||
|
V2Beta4 = "/vac/waku/store/2.0.0-beta4",
|
||||||
|
}
|
@ -5,7 +5,7 @@ import { v4 as uuid } from "uuid";
|
|||||||
import * as protoV2Beta3 from "../../proto/waku/v2/store/v2beta3/store";
|
import * as protoV2Beta3 from "../../proto/waku/v2/store/v2beta3/store";
|
||||||
import * as protoV2Beta4 from "../../proto/waku/v2/store/v2beta4/store";
|
import * as protoV2Beta4 from "../../proto/waku/v2/store/v2beta4/store";
|
||||||
|
|
||||||
import { StoreCodecs } from "./index";
|
import { StoreCodecs } from "./constants";
|
||||||
|
|
||||||
export enum PageDirection {
|
export enum PageDirection {
|
||||||
BACKWARD = "backward",
|
BACKWARD = "backward",
|
||||||
|
@ -6,23 +6,18 @@ import Libp2p from "libp2p";
|
|||||||
import { Peer } from "libp2p/src/peer-store";
|
import { Peer } from "libp2p/src/peer-store";
|
||||||
import PeerId from "peer-id";
|
import PeerId from "peer-id";
|
||||||
|
|
||||||
|
import { DefaultPubSubTopic, StoreCodecs } from "../constants";
|
||||||
import { getPeersForProtocol, selectRandomPeer } from "../select_peer";
|
import { getPeersForProtocol, selectRandomPeer } from "../select_peer";
|
||||||
import { hexToBytes } from "../utils";
|
import { hexToBytes } from "../utils";
|
||||||
import { DefaultPubSubTopic } from "../waku";
|
|
||||||
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
||||||
|
|
||||||
import { HistoryRPC, PageDirection } from "./history_rpc";
|
import { HistoryRPC, PageDirection } from "./history_rpc";
|
||||||
|
|
||||||
const dbg = debug("waku:store");
|
const dbg = debug("waku:store");
|
||||||
|
|
||||||
export enum StoreCodecs {
|
|
||||||
V2Beta3 = "/vac/waku/store/2.0.0-beta3",
|
|
||||||
V2Beta4 = "/vac/waku/store/2.0.0-beta4",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultPageSize = 10;
|
export const DefaultPageSize = 10;
|
||||||
|
|
||||||
export { PageDirection };
|
export { PageDirection, StoreCodecs };
|
||||||
|
|
||||||
export interface CreateOptions {
|
export interface CreateOptions {
|
||||||
/**
|
/**
|
||||||
|
@ -11,8 +11,8 @@ import { Multiaddr, multiaddr } from "multiaddr";
|
|||||||
import PeerId from "peer-id";
|
import PeerId from "peer-id";
|
||||||
import portfinder from "portfinder";
|
import portfinder from "portfinder";
|
||||||
|
|
||||||
|
import { DefaultPubSubTopic } from "../lib/constants";
|
||||||
import { hexToBytes } from "../lib/utils";
|
import { hexToBytes } from "../lib/utils";
|
||||||
import { DefaultPubSubTopic } from "../lib/waku";
|
|
||||||
import { WakuMessage } from "../lib/waku_message";
|
import { WakuMessage } from "../lib/waku_message";
|
||||||
import * as proto from "../proto/waku/v2/message";
|
import * as proto from "../proto/waku/v2/message";
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user