Merge pull request #83 from status-im/community-test
This commit is contained in:
commit
e8a754f418
Binary file not shown.
|
@ -25,6 +25,7 @@
|
|||
"devDependencies": {
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/pbkdf2": "^3.1.0",
|
||||
"@types/secp256k1": "^4.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.1",
|
||||
"@typescript-eslint/parser": "^4.31.1",
|
||||
|
@ -47,6 +48,7 @@
|
|||
"ecies-geth": "^1.5.3",
|
||||
"js-sha3": "^0.8.0",
|
||||
"js-waku": "^0.13.1",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"protobufjs": "^6.11.2",
|
||||
"secp256k1": "^4.0.2"
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ message ChatIdentity {
|
|||
string description = 5;
|
||||
|
||||
string color = 6;
|
||||
|
||||
string emoji = 7;
|
||||
}
|
||||
|
||||
// ProfileImage represents data associated with a user's profile image
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { idToContentTopic } from "./contentTopic";
|
||||
import { createSymKeyFromPassword } from "./encryption";
|
||||
import { ChatMessage, Content } from "./wire/chat_message";
|
||||
import { CommunityChat } from "./wire/community_chat";
|
||||
|
||||
/**
|
||||
* Represent a chat room. Only public chats are currently supported.
|
||||
|
@ -9,16 +10,23 @@ export class Chat {
|
|||
private lastClockValue?: number;
|
||||
private lastMessage?: ChatMessage;
|
||||
|
||||
private constructor(public id: string, public symKey: Uint8Array) {}
|
||||
private constructor(
|
||||
public id: string,
|
||||
public symKey: Uint8Array,
|
||||
public communityChat?: CommunityChat
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a public chat room.
|
||||
* [[Community.instantiateChat]] MUST be used for chats belonging to a community.
|
||||
*/
|
||||
public static async create(id: string): Promise<Chat> {
|
||||
public static async create(
|
||||
id: string,
|
||||
communityChat?: CommunityChat
|
||||
): Promise<Chat> {
|
||||
const symKey = await createSymKeyFromPassword(id);
|
||||
|
||||
return new Chat(id, symKey);
|
||||
return new Chat(id, symKey, communityChat);
|
||||
}
|
||||
|
||||
public get contentTopic(): string {
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { expect } from "chai";
|
||||
import { Waku } from "js-waku";
|
||||
|
||||
import { Community } from "./community";
|
||||
import { CommunityDescription } from "./wire/community_description";
|
||||
|
||||
describe("Community live data", () => {
|
||||
before(function () {
|
||||
if (process.env.CI) {
|
||||
// Skip live data test in CI
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it("Retrieves community description For DappConnect Test from Waku prod fleet", async function () {
|
||||
this.timeout(20000);
|
||||
const waku = await Waku.create({ bootstrap: true });
|
||||
|
||||
await waku.waitForConnectedPeer();
|
||||
|
||||
const community = await Community.instantiateCommunity(
|
||||
"0x0262c65c881f5a9f79343a26faaa02aad3af7c533d9445fb1939ed11b8bf4d2abd",
|
||||
waku
|
||||
);
|
||||
const desc = community.description as CommunityDescription;
|
||||
expect(desc).to.not.be.undefined;
|
||||
|
||||
expect(desc.identity?.displayName).to.eq("DappConnect Test");
|
||||
|
||||
const descChats = Array.from(desc.chats.values()).map(
|
||||
(chat) => chat?.identity?.displayName
|
||||
);
|
||||
expect(descChats).to.include("foobar");
|
||||
expect(descChats).to.include("another-channel!");
|
||||
|
||||
const chats = Array.from(community.chats.values()).map(
|
||||
(chat) => chat?.communityChat?.identity?.displayName
|
||||
);
|
||||
expect(chats).to.include("foobar");
|
||||
expect(chats).to.include("another-channel!");
|
||||
});
|
||||
});
|
|
@ -11,12 +11,13 @@ const dbg = debug("communities:community");
|
|||
export class Community {
|
||||
public publicKey: Uint8Array;
|
||||
private waku: Waku;
|
||||
|
||||
public chats: Map<string, Chat>; // Chat id, Chat
|
||||
public description?: CommunityDescription;
|
||||
|
||||
constructor(publicKey: Uint8Array, waku: Waku) {
|
||||
this.publicKey = publicKey;
|
||||
this.waku = waku;
|
||||
this.chats = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,6 +61,12 @@ export class Community {
|
|||
}
|
||||
|
||||
this.description = desc;
|
||||
|
||||
await Promise.all(
|
||||
Array.from(this.description.chats).map(([chatUuid, communityChat]) => {
|
||||
return this.instantiateChat(chatUuid, communityChat);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,28 +75,18 @@ export class Community {
|
|||
*
|
||||
* @throws string If the Community Description is unavailable or the chat is not found;
|
||||
*/
|
||||
public async instantiateChat(chatName: string): Promise<Chat> {
|
||||
if (!this.description) {
|
||||
await this.refreshCommunityDescription();
|
||||
if (!this.description)
|
||||
throw "Failed to retrieve community description, cannot instantiate chat";
|
||||
}
|
||||
private async instantiateChat(
|
||||
chatUuid: string,
|
||||
communityChat: CommunityChat
|
||||
): Promise<void> {
|
||||
if (!this.description)
|
||||
throw "Failed to retrieve community description, cannot instantiate chat";
|
||||
|
||||
let communityChat: CommunityChat | undefined;
|
||||
let chatUuid: string | undefined;
|
||||
const chatId = this.publicKeyStr + chatUuid;
|
||||
if (this.chats.get(chatId)) return;
|
||||
|
||||
this.description.chats.forEach((_chat, _id) => {
|
||||
if (chatUuid) return;
|
||||
const chat = await Chat.create(chatId, communityChat);
|
||||
|
||||
if (_chat.identity?.displayName === chatName) {
|
||||
chatUuid = _id;
|
||||
communityChat = _chat;
|
||||
}
|
||||
});
|
||||
|
||||
if (!communityChat || !chatUuid)
|
||||
throw `Failed to retrieve community community chat with name ${chatName}`;
|
||||
|
||||
return Chat.create(this.publicKeyStr + chatUuid);
|
||||
this.chats.set(chatId, chat);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { expect } from "chai";
|
||||
|
||||
import { createSymKeyFromPassword } from "./encryption";
|
||||
|
||||
describe("Encryption", () => {
|
||||
it("Generate symmetric key from password", async function () {
|
||||
const str = "arbitrary data here";
|
||||
const symKey = await createSymKeyFromPassword(str);
|
||||
|
||||
expect(Buffer.from(symKey).toString("hex")).to.eq(
|
||||
"c49ad65ebf2a7b7253bf400e3d27719362a91b2c9b9f54d50a69117021666c33"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,9 +1,15 @@
|
|||
import { kdf } from "ecies-geth";
|
||||
import pbkdf2 from "pbkdf2";
|
||||
|
||||
const AESKeyLength = 32; // bytes
|
||||
|
||||
export async function createSymKeyFromPassword(
|
||||
password: string
|
||||
): Promise<Uint8Array> {
|
||||
return kdf(Buffer.from(password, "utf-8"), AESKeyLength);
|
||||
return pbkdf2.pbkdf2Sync(
|
||||
Buffer.from(password, "utf-8"),
|
||||
"",
|
||||
65356,
|
||||
AESKeyLength,
|
||||
"sha256"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface ChatIdentity {
|
|||
/** description is the user set description, valid only for organisations */
|
||||
description: string;
|
||||
color: string;
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
export interface ChatIdentity_ImagesEntry {
|
||||
|
@ -98,6 +99,7 @@ const baseChatIdentity: object = {
|
|||
displayName: "",
|
||||
description: "",
|
||||
color: "",
|
||||
emoji: "",
|
||||
};
|
||||
|
||||
export const ChatIdentity = {
|
||||
|
@ -126,6 +128,9 @@ export const ChatIdentity = {
|
|||
if (message.color !== "") {
|
||||
writer.uint32(50).string(message.color);
|
||||
}
|
||||
if (message.emoji !== "") {
|
||||
writer.uint32(58).string(message.emoji);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
|
@ -161,6 +166,9 @@ export const ChatIdentity = {
|
|||
case 6:
|
||||
message.color = reader.string();
|
||||
break;
|
||||
case 7:
|
||||
message.emoji = reader.string();
|
||||
break;
|
||||
default:
|
||||
reader.skipType(tag & 7);
|
||||
break;
|
||||
|
@ -202,6 +210,11 @@ export const ChatIdentity = {
|
|||
} else {
|
||||
message.color = "";
|
||||
}
|
||||
if (object.emoji !== undefined && object.emoji !== null) {
|
||||
message.emoji = String(object.emoji);
|
||||
} else {
|
||||
message.emoji = "";
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
|
@ -220,6 +233,7 @@ export const ChatIdentity = {
|
|||
message.description !== undefined &&
|
||||
(obj.description = message.description);
|
||||
message.color !== undefined && (obj.color = message.color);
|
||||
message.emoji !== undefined && (obj.emoji = message.emoji);
|
||||
return obj;
|
||||
},
|
||||
|
||||
|
@ -258,6 +272,11 @@ export const ChatIdentity = {
|
|||
} else {
|
||||
message.color = "";
|
||||
}
|
||||
if (object.emoji !== undefined && object.emoji !== null) {
|
||||
message.emoji = object.emoji;
|
||||
} else {
|
||||
message.emoji = "";
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Reader } from "protobufjs";
|
||||
|
||||
import * as proto from "../proto/communities/v1/chat_identity";
|
||||
import { IdentityImage } from "../proto/communities/v1/chat_identity";
|
||||
|
||||
export class ChatIdentity {
|
||||
public constructor(public proto: proto.ChatIdentity) {}
|
||||
|
@ -14,4 +15,37 @@ export class ChatIdentity {
|
|||
encode(): Uint8Array {
|
||||
return proto.ChatIdentity.encode(this.proto).finish();
|
||||
}
|
||||
|
||||
/** Lamport timestamp of the message */
|
||||
get clock(): number | undefined {
|
||||
return this.proto.clock;
|
||||
}
|
||||
|
||||
/** ens_name is the valid ENS name associated with the chat key */
|
||||
get ensName(): string | undefined {
|
||||
return this.proto.ensName;
|
||||
}
|
||||
|
||||
/** images is a string indexed mapping of images associated with an identity */
|
||||
get images(): { [key: string]: IdentityImage } | undefined {
|
||||
return this.proto.images;
|
||||
}
|
||||
|
||||
/** display name is the user set identity, valid only for organisations */
|
||||
get displayName(): string | undefined {
|
||||
return this.proto.displayName;
|
||||
}
|
||||
|
||||
/** description is the user set description, valid only for organisations */
|
||||
get description(): string | undefined {
|
||||
return this.proto.description;
|
||||
}
|
||||
|
||||
get color(): string | undefined {
|
||||
return this.proto.color;
|
||||
}
|
||||
|
||||
get emoji(): string | undefined {
|
||||
return this.proto.emoji;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Reader } from "protobufjs";
|
||||
|
||||
import { ChatIdentity } from "../proto/communities/v1/chat_identity";
|
||||
import * as proto from "../proto/communities/v1/communities";
|
||||
import {
|
||||
CommunityMember,
|
||||
CommunityPermissions,
|
||||
} from "../proto/communities/v1/communities";
|
||||
|
||||
import { ChatIdentity } from "./chat_identity";
|
||||
|
||||
export class CommunityChat {
|
||||
public constructor(public proto: proto.CommunityChat) {}
|
||||
|
||||
|
@ -41,7 +42,9 @@ export class CommunityChat {
|
|||
}
|
||||
|
||||
public get identity(): ChatIdentity | undefined {
|
||||
return this.proto.identity;
|
||||
if (!this.proto.identity) return;
|
||||
|
||||
return new ChatIdentity(this.proto.identity);
|
||||
}
|
||||
|
||||
// TODO: Document this
|
||||
|
|
|
@ -3,9 +3,11 @@ import { WakuMessage, WakuStore } from "js-waku";
|
|||
import { Reader } from "protobufjs";
|
||||
|
||||
import { idToContentTopic } from "../contentTopic";
|
||||
import { createSymKeyFromPassword } from "../encryption";
|
||||
import * as proto from "../proto/communities/v1/communities";
|
||||
import { bufToHex } from "../utils";
|
||||
|
||||
import { ApplicationMetadataMessage } from "./application_metadata_message";
|
||||
import { ChatIdentity } from "./chat_identity";
|
||||
import { CommunityChat } from "./community_chat";
|
||||
|
||||
|
@ -45,8 +47,11 @@ export class CommunityDescription {
|
|||
orderedMessages.forEach((message: WakuMessage) => {
|
||||
if (!message.payload) return;
|
||||
try {
|
||||
const metadata = ApplicationMetadataMessage.decode(message.payload);
|
||||
if (!metadata.payload) return;
|
||||
|
||||
const _communityDescription = CommunityDescription.decode(
|
||||
message.payload
|
||||
metadata.payload
|
||||
);
|
||||
|
||||
if (!_communityDescription.identity) return;
|
||||
|
@ -61,9 +66,12 @@ export class CommunityDescription {
|
|||
});
|
||||
};
|
||||
|
||||
const symKey = await createSymKeyFromPassword(hexCommunityPublicKey);
|
||||
|
||||
await wakuStore
|
||||
.queryHistory([contentTopic], {
|
||||
callback,
|
||||
decryptionKeys: [symKey],
|
||||
})
|
||||
.catch((e) => {
|
||||
dbg(
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -938,6 +938,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/pbkdf2@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@types/pbkdf2@npm:3.1.0"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: d15024b1957c21cf3b8887329d9bd8dfde754cf13a09d76ae25f1391cfc62bb8b8d7b760773c5dbaa748172fba8b3e0c3dbe962af6ccbd69b76df12a48dfba40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prettier@npm:^1.19.0":
|
||||
version: 1.19.1
|
||||
resolution: "@types/prettier@npm:1.19.1"
|
||||
|
@ -8755,7 +8764,7 @@ fsevents@~2.3.2:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pbkdf2@npm:^3.0.3":
|
||||
"pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "pbkdf2@npm:3.1.2"
|
||||
dependencies:
|
||||
|
@ -10487,6 +10496,7 @@ resolve@^2.0.0-next.3:
|
|||
dependencies:
|
||||
"@types/chai": ^4.2.22
|
||||
"@types/mocha": ^9.0.0
|
||||
"@types/pbkdf2": ^3.1.0
|
||||
"@types/secp256k1": ^4.0.3
|
||||
"@typescript-eslint/eslint-plugin": ^4.31.1
|
||||
"@typescript-eslint/parser": ^4.31.1
|
||||
|
@ -10503,6 +10513,7 @@ resolve@^2.0.0-next.3:
|
|||
js-waku: ^0.13.1
|
||||
mocha: ^9.1.1
|
||||
npm-run-all: ^4.1.5
|
||||
pbkdf2: ^3.1.2
|
||||
prettier: ^2.4.0
|
||||
protobufjs: ^6.11.2
|
||||
secp256k1: ^4.0.2
|
||||
|
|
Loading…
Reference in New Issue