Merge pull request #83 from status-im/community-test

This commit is contained in:
Franck Royer 2021-10-19 15:00:37 +11:00 committed by GitHub
commit e8a754f418
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 176 additions and 30 deletions

Binary file not shown.

View File

@ -25,6 +25,7 @@
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.22", "@types/chai": "^4.2.22",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@types/pbkdf2": "^3.1.0",
"@types/secp256k1": "^4.0.3", "@types/secp256k1": "^4.0.3",
"@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1", "@typescript-eslint/parser": "^4.31.1",
@ -47,6 +48,7 @@
"ecies-geth": "^1.5.3", "ecies-geth": "^1.5.3",
"js-sha3": "^0.8.0", "js-sha3": "^0.8.0",
"js-waku": "^0.13.1", "js-waku": "^0.13.1",
"pbkdf2": "^3.1.2",
"protobufjs": "^6.11.2", "protobufjs": "^6.11.2",
"secp256k1": "^4.0.2" "secp256k1": "^4.0.2"
} }

View File

@ -22,6 +22,8 @@ message ChatIdentity {
string description = 5; string description = 5;
string color = 6; string color = 6;
string emoji = 7;
} }
// ProfileImage represents data associated with a user's profile image // ProfileImage represents data associated with a user's profile image

View File

@ -1,6 +1,7 @@
import { idToContentTopic } from "./contentTopic"; import { idToContentTopic } from "./contentTopic";
import { createSymKeyFromPassword } from "./encryption"; import { createSymKeyFromPassword } from "./encryption";
import { ChatMessage, Content } from "./wire/chat_message"; import { ChatMessage, Content } from "./wire/chat_message";
import { CommunityChat } from "./wire/community_chat";
/** /**
* Represent a chat room. Only public chats are currently supported. * Represent a chat room. Only public chats are currently supported.
@ -9,16 +10,23 @@ export class Chat {
private lastClockValue?: number; private lastClockValue?: number;
private lastMessage?: ChatMessage; 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. * Create a public chat room.
* [[Community.instantiateChat]] MUST be used for chats belonging to a community. * [[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); const symKey = await createSymKeyFromPassword(id);
return new Chat(id, symKey); return new Chat(id, symKey, communityChat);
} }
public get contentTopic(): string { public get contentTopic(): string {

View File

@ -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!");
});
});

View File

@ -11,12 +11,13 @@ const dbg = debug("communities:community");
export class Community { export class Community {
public publicKey: Uint8Array; public publicKey: Uint8Array;
private waku: Waku; private waku: Waku;
public chats: Map<string, Chat>; // Chat id, Chat
public description?: CommunityDescription; public description?: CommunityDescription;
constructor(publicKey: Uint8Array, waku: Waku) { constructor(publicKey: Uint8Array, waku: Waku) {
this.publicKey = publicKey; this.publicKey = publicKey;
this.waku = waku; this.waku = waku;
this.chats = new Map();
} }
/** /**
@ -60,6 +61,12 @@ export class Community {
} }
this.description = desc; 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; * @throws string If the Community Description is unavailable or the chat is not found;
*/ */
public async instantiateChat(chatName: string): Promise<Chat> { private async instantiateChat(
if (!this.description) { chatUuid: string,
await this.refreshCommunityDescription(); communityChat: CommunityChat
if (!this.description) ): Promise<void> {
throw "Failed to retrieve community description, cannot instantiate chat"; if (!this.description)
} throw "Failed to retrieve community description, cannot instantiate chat";
let communityChat: CommunityChat | undefined; const chatId = this.publicKeyStr + chatUuid;
let chatUuid: string | undefined; if (this.chats.get(chatId)) return;
this.description.chats.forEach((_chat, _id) => { const chat = await Chat.create(chatId, communityChat);
if (chatUuid) return;
if (_chat.identity?.displayName === chatName) { this.chats.set(chatId, chat);
chatUuid = _id;
communityChat = _chat;
}
});
if (!communityChat || !chatUuid)
throw `Failed to retrieve community community chat with name ${chatName}`;
return Chat.create(this.publicKeyStr + chatUuid);
} }
} }

View File

@ -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"
);
});
});

View File

@ -1,9 +1,15 @@
import { kdf } from "ecies-geth"; import pbkdf2 from "pbkdf2";
const AESKeyLength = 32; // bytes const AESKeyLength = 32; // bytes
export async function createSymKeyFromPassword( export async function createSymKeyFromPassword(
password: string password: string
): Promise<Uint8Array> { ): Promise<Uint8Array> {
return kdf(Buffer.from(password, "utf-8"), AESKeyLength); return pbkdf2.pbkdf2Sync(
Buffer.from(password, "utf-8"),
"",
65356,
AESKeyLength,
"sha256"
);
} }

View File

@ -22,6 +22,7 @@ export interface ChatIdentity {
/** description is the user set description, valid only for organisations */ /** description is the user set description, valid only for organisations */
description: string; description: string;
color: string; color: string;
emoji: string;
} }
export interface ChatIdentity_ImagesEntry { export interface ChatIdentity_ImagesEntry {
@ -98,6 +99,7 @@ const baseChatIdentity: object = {
displayName: "", displayName: "",
description: "", description: "",
color: "", color: "",
emoji: "",
}; };
export const ChatIdentity = { export const ChatIdentity = {
@ -126,6 +128,9 @@ export const ChatIdentity = {
if (message.color !== "") { if (message.color !== "") {
writer.uint32(50).string(message.color); writer.uint32(50).string(message.color);
} }
if (message.emoji !== "") {
writer.uint32(58).string(message.emoji);
}
return writer; return writer;
}, },
@ -161,6 +166,9 @@ export const ChatIdentity = {
case 6: case 6:
message.color = reader.string(); message.color = reader.string();
break; break;
case 7:
message.emoji = reader.string();
break;
default: default:
reader.skipType(tag & 7); reader.skipType(tag & 7);
break; break;
@ -202,6 +210,11 @@ export const ChatIdentity = {
} else { } else {
message.color = ""; message.color = "";
} }
if (object.emoji !== undefined && object.emoji !== null) {
message.emoji = String(object.emoji);
} else {
message.emoji = "";
}
return message; return message;
}, },
@ -220,6 +233,7 @@ export const ChatIdentity = {
message.description !== undefined && message.description !== undefined &&
(obj.description = message.description); (obj.description = message.description);
message.color !== undefined && (obj.color = message.color); message.color !== undefined && (obj.color = message.color);
message.emoji !== undefined && (obj.emoji = message.emoji);
return obj; return obj;
}, },
@ -258,6 +272,11 @@ export const ChatIdentity = {
} else { } else {
message.color = ""; message.color = "";
} }
if (object.emoji !== undefined && object.emoji !== null) {
message.emoji = object.emoji;
} else {
message.emoji = "";
}
return message; return message;
}, },
}; };

View File

@ -1,6 +1,7 @@
import { Reader } from "protobufjs"; import { Reader } from "protobufjs";
import * as proto from "../proto/communities/v1/chat_identity"; import * as proto from "../proto/communities/v1/chat_identity";
import { IdentityImage } from "../proto/communities/v1/chat_identity";
export class ChatIdentity { export class ChatIdentity {
public constructor(public proto: proto.ChatIdentity) {} public constructor(public proto: proto.ChatIdentity) {}
@ -14,4 +15,37 @@ export class ChatIdentity {
encode(): Uint8Array { encode(): Uint8Array {
return proto.ChatIdentity.encode(this.proto).finish(); 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;
}
} }

View File

@ -1,12 +1,13 @@
import { Reader } from "protobufjs"; import { Reader } from "protobufjs";
import { ChatIdentity } from "../proto/communities/v1/chat_identity";
import * as proto from "../proto/communities/v1/communities"; import * as proto from "../proto/communities/v1/communities";
import { import {
CommunityMember, CommunityMember,
CommunityPermissions, CommunityPermissions,
} from "../proto/communities/v1/communities"; } from "../proto/communities/v1/communities";
import { ChatIdentity } from "./chat_identity";
export class CommunityChat { export class CommunityChat {
public constructor(public proto: proto.CommunityChat) {} public constructor(public proto: proto.CommunityChat) {}
@ -41,7 +42,9 @@ export class CommunityChat {
} }
public get identity(): ChatIdentity | undefined { public get identity(): ChatIdentity | undefined {
return this.proto.identity; if (!this.proto.identity) return;
return new ChatIdentity(this.proto.identity);
} }
// TODO: Document this // TODO: Document this

View File

@ -3,9 +3,11 @@ import { WakuMessage, WakuStore } from "js-waku";
import { Reader } from "protobufjs"; import { Reader } from "protobufjs";
import { idToContentTopic } from "../contentTopic"; import { idToContentTopic } from "../contentTopic";
import { createSymKeyFromPassword } from "../encryption";
import * as proto from "../proto/communities/v1/communities"; import * as proto from "../proto/communities/v1/communities";
import { bufToHex } from "../utils"; import { bufToHex } from "../utils";
import { ApplicationMetadataMessage } from "./application_metadata_message";
import { ChatIdentity } from "./chat_identity"; import { ChatIdentity } from "./chat_identity";
import { CommunityChat } from "./community_chat"; import { CommunityChat } from "./community_chat";
@ -45,8 +47,11 @@ export class CommunityDescription {
orderedMessages.forEach((message: WakuMessage) => { orderedMessages.forEach((message: WakuMessage) => {
if (!message.payload) return; if (!message.payload) return;
try { try {
const metadata = ApplicationMetadataMessage.decode(message.payload);
if (!metadata.payload) return;
const _communityDescription = CommunityDescription.decode( const _communityDescription = CommunityDescription.decode(
message.payload metadata.payload
); );
if (!_communityDescription.identity) return; if (!_communityDescription.identity) return;
@ -61,9 +66,12 @@ export class CommunityDescription {
}); });
}; };
const symKey = await createSymKeyFromPassword(hexCommunityPublicKey);
await wakuStore await wakuStore
.queryHistory([contentTopic], { .queryHistory([contentTopic], {
callback, callback,
decryptionKeys: [symKey],
}) })
.catch((e) => { .catch((e) => {
dbg( dbg(

View File

@ -938,6 +938,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@types/prettier@npm:^1.19.0":
version: 1.19.1 version: 1.19.1
resolution: "@types/prettier@npm:1.19.1" resolution: "@types/prettier@npm:1.19.1"
@ -8755,7 +8764,7 @@ fsevents@~2.3.2:
languageName: node languageName: node
linkType: hard linkType: hard
"pbkdf2@npm:^3.0.3": "pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2":
version: 3.1.2 version: 3.1.2
resolution: "pbkdf2@npm:3.1.2" resolution: "pbkdf2@npm:3.1.2"
dependencies: dependencies:
@ -10487,6 +10496,7 @@ resolve@^2.0.0-next.3:
dependencies: dependencies:
"@types/chai": ^4.2.22 "@types/chai": ^4.2.22
"@types/mocha": ^9.0.0 "@types/mocha": ^9.0.0
"@types/pbkdf2": ^3.1.0
"@types/secp256k1": ^4.0.3 "@types/secp256k1": ^4.0.3
"@typescript-eslint/eslint-plugin": ^4.31.1 "@typescript-eslint/eslint-plugin": ^4.31.1
"@typescript-eslint/parser": ^4.31.1 "@typescript-eslint/parser": ^4.31.1
@ -10503,6 +10513,7 @@ resolve@^2.0.0-next.3:
js-waku: ^0.13.1 js-waku: ^0.13.1
mocha: ^9.1.1 mocha: ^9.1.1
npm-run-all: ^4.1.5 npm-run-all: ^4.1.5
pbkdf2: ^3.1.2
prettier: ^2.4.0 prettier: ^2.4.0
protobufjs: ^6.11.2 protobufjs: ^6.11.2
secp256k1: ^4.0.2 secp256k1: ^4.0.2