Merge pull request #42 from status-im/create-community

This commit is contained in:
Franck Royer 2021-10-08 14:51:19 +11:00 committed by GitHub
commit d645eadba4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 2743 additions and 33 deletions

View File

@ -2,7 +2,7 @@
import { getBootstrapNodes, StoreCodec } from "js-waku"; import { getBootstrapNodes, StoreCodec } from "js-waku";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Identity, Messenger } from "status-communities/dist/cjs"; import { Identity, Messenger } from "status-communities/dist/cjs";
import { ApplicationMetadataMessage } from "status-communities/dist/cjs/application_metadata_message"; import { ApplicationMetadataMessage } from "status-communities/dist/cjs";
import { uintToImgUrl } from "../helpers/uintToImgUrl"; import { uintToImgUrl } from "../helpers/uintToImgUrl";
import { ChatMessage } from "../models/ChatMessage"; import { ChatMessage } from "../models/ChatMessage";

View File

@ -14,7 +14,7 @@
], ],
"globals": { "BigInt": true, "console": true, "WebAssembly": true }, "globals": { "BigInt": true, "console": true, "WebAssembly": true },
"rules": { "rules": {
"@typescript-eslint/explicit-function-return-type": ["warn"], "@typescript-eslint/explicit-function-return-type": ["error"],
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [ "eslint-comments/disable-enable-pair": [
"error", "error",

View File

@ -3,3 +3,7 @@ version: v1beta1
build: build:
roots: roots:
- ./proto - ./proto
lint:
except:
- ENUM_ZERO_VALUE_SUFFIX
- ENUM_VALUE_PREFIX

View File

@ -0,0 +1,53 @@
syntax = "proto3";
package communities.v1;
import "communities/v1/enums.proto";
// ChatIdentity represents the user defined identity associated with their public chat key
message ChatIdentity {
// Lamport timestamp of the message
uint64 clock = 1;
// ens_name is the valid ENS name associated with the chat key
string ens_name = 2;
// images is a string indexed mapping of images associated with an identity
map<string, IdentityImage> images = 3;
// display name is the user set identity, valid only for organisations
string display_name = 4;
// description is the user set description, valid only for organisations
string description = 5;
string color = 6;
}
// ProfileImage represents data associated with a user's profile image
message IdentityImage {
// payload is a context based payload for the profile image data,
// context is determined by the `source_type`
bytes payload = 1;
// source_type signals the image payload source
SourceType source_type = 2;
// image_type signals the image type and method of parsing the payload
ImageType image_type =3;
// SourceType are the predefined types of image source allowed
enum SourceType {
UNKNOWN_SOURCE_TYPE = 0;
// RAW_PAYLOAD image byte data
RAW_PAYLOAD = 1;
// ENS_AVATAR uses the ENS record's resolver get-text-data.avatar data
// The `payload` field will be ignored if ENS_AVATAR is selected
// The application will read and parse the ENS avatar data as image payload data, URLs will be ignored
// The parent `ChatMessageIdentity` must have a valid `ens_name` set
ENS_AVATAR = 2;
}
}

View File

@ -0,0 +1,80 @@
syntax = "proto3";
package communities.v1;
import "communities/v1/chat_identity.proto";
message Grant {
bytes community_id = 1;
bytes member_id = 2;
string chat_id = 3;
uint64 clock = 4;
}
message CommunityMember {
enum Roles {
UNKNOWN_ROLE = 0;
ROLE_ALL = 1;
ROLE_MANAGE_USERS = 2;
}
repeated Roles roles = 1;
}
message CommunityPermissions {
enum Access {
UNKNOWN_ACCESS = 0;
NO_MEMBERSHIP = 1;
INVITATION_ONLY = 2;
ON_REQUEST = 3;
}
bool ens_only = 1;
// https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md is a candidate for the algorithm to be used in case we want to have private communityal chats, lighter than pairwise encryption using the DR, less secure, but more efficient for large number of participants
bool private = 2;
Access access = 3;
}
message CommunityDescription {
uint64 clock = 1;
map<string,CommunityMember> members = 2;
CommunityPermissions permissions = 3;
ChatIdentity identity = 5;
map<string,CommunityChat> chats = 6;
repeated string ban_list = 7;
map<string,CommunityCategory> categories = 8;
}
message CommunityChat {
map<string,CommunityMember> members = 1;
CommunityPermissions permissions = 2;
ChatIdentity identity = 3;
string category_id = 4;
int32 position = 5;
}
message CommunityCategory {
string category_id = 1;
string name = 2;
int32 position = 3;
}
message CommunityInvitation {
bytes community_description = 1;
bytes grant = 2;
string chat_id = 3;
bytes public_key = 4;
}
message CommunityRequestToJoin {
uint64 clock = 1;
string ens_name = 2;
string chat_id = 3;
bytes community_id = 4;
}
message CommunityRequestToJoinResponse {
uint64 clock = 1;
CommunityDescription community = 2;
bool accepted = 3;
bytes grant = 4;
}

View File

@ -1,6 +1,6 @@
import { ChatMessage, Content } from "./chat_message"; import { idToContentTopic } from "./contentTopic";
import { chatIdToContentTopic } from "./contentTopic";
import { createSymKeyFromPassword } from "./encryption"; import { createSymKeyFromPassword } from "./encryption";
import { ChatMessage, Content } from "./wire/chat_message";
/** /**
* Represent a chat room. Only public chats are currently supported. * Represent a chat room. Only public chats are currently supported.
@ -13,6 +13,7 @@ export class Chat {
/** /**
* Create a public chat room. * 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): Promise<Chat> {
const symKey = await createSymKeyFromPassword(id); const symKey = await createSymKeyFromPassword(id);
@ -21,7 +22,7 @@ export class Chat {
} }
public get contentTopic(): string { public get contentTopic(): string {
return chatIdToContentTopic(this.id); return idToContentTopic(this.id);
} }
public createMessage(content: Content): ChatMessage { public createMessage(content: Content): ChatMessage {

View File

@ -1 +1,95 @@
export class Community {} import debug from "debug";
import { Waku } from "js-waku";
import { Chat } from "./chat";
import { bufToHex, hexToBuf } from "./utils";
import { CommunityChat } from "./wire/community_chat";
import { CommunityDescription } from "./wire/community_description";
const dbg = debug("communities:community");
export class Community {
public publicKey: Uint8Array;
private waku: Waku;
public description?: CommunityDescription;
constructor(publicKey: Uint8Array, waku: Waku) {
this.publicKey = publicKey;
this.waku = waku;
}
/**
* Instantiate a Community by retrieving its details from the Waku network.
*
* This class is used to interact with existing communities only,
* the Status Desktop or Mobile app must be used to manage a community.
*
* @param publicKey The community's public key in hex format.
* Can be found in the community's invite link: https://join.status.im/c/<public key>
* @param waku The Waku instance, used to retrieve Community information from the network.
*/
public async instantiateCommunity(
publicKey: string,
waku: Waku
): Promise<Community> {
const community = new Community(hexToBuf(publicKey), waku);
await community.refreshCommunityDescription();
return community;
}
public get publicKeyStr(): string {
return bufToHex(this.publicKey);
}
/**
* Retrieve and update community information from the network.
* Uses most recent community description message available.
*/
async refreshCommunityDescription(): Promise<void> {
const desc = await CommunityDescription.retrieve(
this.publicKey,
this.waku.store
);
if (!desc) {
dbg(`Failed to retrieve Community Description for ${this.publicKeyStr}`);
return;
}
this.description = desc;
}
/**
* Instantiate [[Chat]] object based on the passed chat name.
* The Chat MUST already be part of the Community and the name MUST be exact (including casing).
*
* @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";
}
let communityChat: CommunityChat | undefined;
let chatUuid: string | undefined;
this.description.chats.forEach((_chat, _id) => {
if (chatUuid) return;
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);
}
}

View File

@ -4,8 +4,13 @@ import { keccak256 } from "js-sha3";
const TopicLength = 4; const TopicLength = 4;
export function chatIdToContentTopic(chatId: string): string { /**
const hash = keccak256.arrayBuffer(chatId); * Get the content topic of for a given Chat or Community
* @param id The Chat id or Community id (hex string prefixed with 0x).
* @returns string The Waku v2 Content Topic.
*/
export function idToContentTopic(id: string): string {
const hash = keccak256.arrayBuffer(id);
const topic = Buffer.from(hash).slice(0, TopicLength); const topic = Buffer.from(hash).slice(0, TopicLength);

View File

@ -2,9 +2,10 @@ import { Buffer } from "buffer";
import { keccak256 } from "js-sha3"; import { keccak256 } from "js-sha3";
import { generatePrivateKey } from "js-waku"; import { generatePrivateKey } from "js-waku";
import { utils } from "js-waku";
import * as secp256k1 from "secp256k1"; import * as secp256k1 from "secp256k1";
import { hexToBuf } from "./utils";
export class Identity { export class Identity {
public constructor(public privateKey: Uint8Array) {} public constructor(public privateKey: Uint8Array) {}
@ -20,7 +21,7 @@ export class Identity {
const hash = keccak256(payload); const hash = keccak256(payload);
const { signature, recid } = secp256k1.ecdsaSign( const { signature, recid } = secp256k1.ecdsaSign(
utils.hexToBuf(hash), hexToBuf(hash),
this.privateKey this.privateKey
); );

View File

@ -1,3 +1,15 @@
import { Identity } from "./identity"; export { Identity } from "./identity";
import { Messenger } from "./messenger"; export { Messenger } from "./messenger";
export { Messenger, Identity }; export { Community } from "./community";
export { Chat } from "./chat";
export * as utils from "./utils";
export { ApplicationMetadataMessage } from "./wire/application_metadata_message";
export {
ChatMessage,
ContentType,
Content,
StickerContent,
ImageContent,
AudioContent,
TextContent,
} from "./wire/chat_message";

View File

@ -1,11 +1,11 @@
import { expect } from "chai"; import { expect } from "chai";
import debug from "debug"; import debug from "debug";
import { utils } from "js-waku";
import { ApplicationMetadataMessage } from "./application_metadata_message";
import { ContentType } from "./chat_message";
import { Identity } from "./identity"; import { Identity } from "./identity";
import { Messenger } from "./messenger"; import { Messenger } from "./messenger";
import { bufToHex } from "./utils";
import { ApplicationMetadataMessage } from "./wire/application_metadata_message";
import { ContentType } from "./wire/chat_message";
const testChatId = "test-chat-id"; const testChatId = "test-chat-id";
@ -106,8 +106,8 @@ describe("Messenger", () => {
const receivedMessage = await receivedMessagePromise; const receivedMessage = await receivedMessagePromise;
expect(utils.bufToHex(receivedMessage.signer!)).to.eq( expect(bufToHex(receivedMessage.signer!)).to.eq(
utils.bufToHex(identityAlice.publicKey) bufToHex(identityAlice.publicKey)
); );
}); });

View File

@ -2,11 +2,11 @@ import debug from "debug";
import { Waku, WakuMessage } from "js-waku"; import { Waku, WakuMessage } from "js-waku";
import { CreateOptions as WakuCreateOptions } from "js-waku/build/main/lib/waku"; import { CreateOptions as WakuCreateOptions } from "js-waku/build/main/lib/waku";
import { ApplicationMetadataMessage } from "./application_metadata_message";
import { Chat } from "./chat"; import { Chat } from "./chat";
import { ChatMessage, Content } from "./chat_message";
import { Identity } from "./identity"; import { Identity } from "./identity";
import { ApplicationMetadataMessage_Type } from "./proto/status/v1/application_metadata_message"; import { ApplicationMetadataMessage_Type } from "./proto/status/v1/application_metadata_message";
import { ApplicationMetadataMessage } from "./wire/application_metadata_message";
import { ChatMessage, Content } from "./wire/chat_message";
const dbg = debug("communities:messenger"); const dbg = debug("communities:messenger");

View File

@ -0,0 +1,513 @@
/* eslint-disable */
import Long from "long";
import _m0 from "protobufjs/minimal";
import {
ImageType,
imageTypeFromJSON,
imageTypeToJSON,
} from "../../communities/v1/enums";
export const protobufPackage = "communities.v1";
/** ChatIdentity represents the user defined identity associated with their public chat key */
export interface ChatIdentity {
/** Lamport timestamp of the message */
clock: number;
/** ens_name is the valid ENS name associated with the chat key */
ensName: string;
/** images is a string indexed mapping of images associated with an identity */
images: { [key: string]: IdentityImage };
/** display name is the user set identity, valid only for organisations */
displayName: string;
/** description is the user set description, valid only for organisations */
description: string;
color: string;
}
export interface ChatIdentity_ImagesEntry {
key: string;
value: IdentityImage | undefined;
}
/** ProfileImage represents data associated with a user's profile image */
export interface IdentityImage {
/**
* payload is a context based payload for the profile image data,
* context is determined by the `source_type`
*/
payload: Uint8Array;
/** source_type signals the image payload source */
sourceType: IdentityImage_SourceType;
/** image_type signals the image type and method of parsing the payload */
imageType: ImageType;
}
/** SourceType are the predefined types of image source allowed */
export enum IdentityImage_SourceType {
UNKNOWN_SOURCE_TYPE = 0,
/** RAW_PAYLOAD - RAW_PAYLOAD image byte data */
RAW_PAYLOAD = 1,
/**
* ENS_AVATAR - ENS_AVATAR uses the ENS record's resolver get-text-data.avatar data
* The `payload` field will be ignored if ENS_AVATAR is selected
* The application will read and parse the ENS avatar data as image payload data, URLs will be ignored
* The parent `ChatMessageIdentity` must have a valid `ens_name` set
*/
ENS_AVATAR = 2,
UNRECOGNIZED = -1,
}
export function identityImage_SourceTypeFromJSON(
object: any
): IdentityImage_SourceType {
switch (object) {
case 0:
case "UNKNOWN_SOURCE_TYPE":
return IdentityImage_SourceType.UNKNOWN_SOURCE_TYPE;
case 1:
case "RAW_PAYLOAD":
return IdentityImage_SourceType.RAW_PAYLOAD;
case 2:
case "ENS_AVATAR":
return IdentityImage_SourceType.ENS_AVATAR;
case -1:
case "UNRECOGNIZED":
default:
return IdentityImage_SourceType.UNRECOGNIZED;
}
}
export function identityImage_SourceTypeToJSON(
object: IdentityImage_SourceType
): string {
switch (object) {
case IdentityImage_SourceType.UNKNOWN_SOURCE_TYPE:
return "UNKNOWN_SOURCE_TYPE";
case IdentityImage_SourceType.RAW_PAYLOAD:
return "RAW_PAYLOAD";
case IdentityImage_SourceType.ENS_AVATAR:
return "ENS_AVATAR";
default:
return "UNKNOWN";
}
}
const baseChatIdentity: object = {
clock: 0,
ensName: "",
displayName: "",
description: "",
color: "",
};
export const ChatIdentity = {
encode(
message: ChatIdentity,
writer: _m0.Writer = _m0.Writer.create()
): _m0.Writer {
if (message.clock !== 0) {
writer.uint32(8).uint64(message.clock);
}
if (message.ensName !== "") {
writer.uint32(18).string(message.ensName);
}
Object.entries(message.images).forEach(([key, value]) => {
ChatIdentity_ImagesEntry.encode(
{ key: key as any, value },
writer.uint32(26).fork()
).ldelim();
});
if (message.displayName !== "") {
writer.uint32(34).string(message.displayName);
}
if (message.description !== "") {
writer.uint32(42).string(message.description);
}
if (message.color !== "") {
writer.uint32(50).string(message.color);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): ChatIdentity {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = { ...baseChatIdentity } as ChatIdentity;
message.images = {};
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.clock = longToNumber(reader.uint64() as Long);
break;
case 2:
message.ensName = reader.string();
break;
case 3:
const entry3 = ChatIdentity_ImagesEntry.decode(
reader,
reader.uint32()
);
if (entry3.value !== undefined) {
message.images[entry3.key] = entry3.value;
}
break;
case 4:
message.displayName = reader.string();
break;
case 5:
message.description = reader.string();
break;
case 6:
message.color = reader.string();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},
fromJSON(object: any): ChatIdentity {
const message = { ...baseChatIdentity } as ChatIdentity;
message.images = {};
if (object.clock !== undefined && object.clock !== null) {
message.clock = Number(object.clock);
} else {
message.clock = 0;
}
if (object.ensName !== undefined && object.ensName !== null) {
message.ensName = String(object.ensName);
} else {
message.ensName = "";
}
if (object.images !== undefined && object.images !== null) {
Object.entries(object.images).forEach(([key, value]) => {
message.images[key] = IdentityImage.fromJSON(value);
});
}
if (object.displayName !== undefined && object.displayName !== null) {
message.displayName = String(object.displayName);
} else {
message.displayName = "";
}
if (object.description !== undefined && object.description !== null) {
message.description = String(object.description);
} else {
message.description = "";
}
if (object.color !== undefined && object.color !== null) {
message.color = String(object.color);
} else {
message.color = "";
}
return message;
},
toJSON(message: ChatIdentity): unknown {
const obj: any = {};
message.clock !== undefined && (obj.clock = message.clock);
message.ensName !== undefined && (obj.ensName = message.ensName);
obj.images = {};
if (message.images) {
Object.entries(message.images).forEach(([k, v]) => {
obj.images[k] = IdentityImage.toJSON(v);
});
}
message.displayName !== undefined &&
(obj.displayName = message.displayName);
message.description !== undefined &&
(obj.description = message.description);
message.color !== undefined && (obj.color = message.color);
return obj;
},
fromPartial(object: DeepPartial<ChatIdentity>): ChatIdentity {
const message = { ...baseChatIdentity } as ChatIdentity;
message.images = {};
if (object.clock !== undefined && object.clock !== null) {
message.clock = object.clock;
} else {
message.clock = 0;
}
if (object.ensName !== undefined && object.ensName !== null) {
message.ensName = object.ensName;
} else {
message.ensName = "";
}
if (object.images !== undefined && object.images !== null) {
Object.entries(object.images).forEach(([key, value]) => {
if (value !== undefined) {
message.images[key] = IdentityImage.fromPartial(value);
}
});
}
if (object.displayName !== undefined && object.displayName !== null) {
message.displayName = object.displayName;
} else {
message.displayName = "";
}
if (object.description !== undefined && object.description !== null) {
message.description = object.description;
} else {
message.description = "";
}
if (object.color !== undefined && object.color !== null) {
message.color = object.color;
} else {
message.color = "";
}
return message;
},
};
const baseChatIdentity_ImagesEntry: object = { key: "" };
export const ChatIdentity_ImagesEntry = {
encode(
message: ChatIdentity_ImagesEntry,
writer: _m0.Writer = _m0.Writer.create()
): _m0.Writer {
if (message.key !== "") {
writer.uint32(10).string(message.key);
}
if (message.value !== undefined) {
IdentityImage.encode(message.value, writer.uint32(18).fork()).ldelim();
}
return writer;
},
decode(
input: _m0.Reader | Uint8Array,
length?: number
): ChatIdentity_ImagesEntry {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = {
...baseChatIdentity_ImagesEntry,
} as ChatIdentity_ImagesEntry;
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.key = reader.string();
break;
case 2:
message.value = IdentityImage.decode(reader, reader.uint32());
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},
fromJSON(object: any): ChatIdentity_ImagesEntry {
const message = {
...baseChatIdentity_ImagesEntry,
} as ChatIdentity_ImagesEntry;
if (object.key !== undefined && object.key !== null) {
message.key = String(object.key);
} else {
message.key = "";
}
if (object.value !== undefined && object.value !== null) {
message.value = IdentityImage.fromJSON(object.value);
} else {
message.value = undefined;
}
return message;
},
toJSON(message: ChatIdentity_ImagesEntry): unknown {
const obj: any = {};
message.key !== undefined && (obj.key = message.key);
message.value !== undefined &&
(obj.value = message.value
? IdentityImage.toJSON(message.value)
: undefined);
return obj;
},
fromPartial(
object: DeepPartial<ChatIdentity_ImagesEntry>
): ChatIdentity_ImagesEntry {
const message = {
...baseChatIdentity_ImagesEntry,
} as ChatIdentity_ImagesEntry;
if (object.key !== undefined && object.key !== null) {
message.key = object.key;
} else {
message.key = "";
}
if (object.value !== undefined && object.value !== null) {
message.value = IdentityImage.fromPartial(object.value);
} else {
message.value = undefined;
}
return message;
},
};
const baseIdentityImage: object = { sourceType: 0, imageType: 0 };
export const IdentityImage = {
encode(
message: IdentityImage,
writer: _m0.Writer = _m0.Writer.create()
): _m0.Writer {
if (message.payload.length !== 0) {
writer.uint32(10).bytes(message.payload);
}
if (message.sourceType !== 0) {
writer.uint32(16).int32(message.sourceType);
}
if (message.imageType !== 0) {
writer.uint32(24).int32(message.imageType);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): IdentityImage {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = { ...baseIdentityImage } as IdentityImage;
message.payload = new Uint8Array();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.payload = reader.bytes();
break;
case 2:
message.sourceType = reader.int32() as any;
break;
case 3:
message.imageType = reader.int32() as any;
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},
fromJSON(object: any): IdentityImage {
const message = { ...baseIdentityImage } as IdentityImage;
message.payload = new Uint8Array();
if (object.payload !== undefined && object.payload !== null) {
message.payload = bytesFromBase64(object.payload);
}
if (object.sourceType !== undefined && object.sourceType !== null) {
message.sourceType = identityImage_SourceTypeFromJSON(object.sourceType);
} else {
message.sourceType = 0;
}
if (object.imageType !== undefined && object.imageType !== null) {
message.imageType = imageTypeFromJSON(object.imageType);
} else {
message.imageType = 0;
}
return message;
},
toJSON(message: IdentityImage): unknown {
const obj: any = {};
message.payload !== undefined &&
(obj.payload = base64FromBytes(
message.payload !== undefined ? message.payload : new Uint8Array()
));
message.sourceType !== undefined &&
(obj.sourceType = identityImage_SourceTypeToJSON(message.sourceType));
message.imageType !== undefined &&
(obj.imageType = imageTypeToJSON(message.imageType));
return obj;
},
fromPartial(object: DeepPartial<IdentityImage>): IdentityImage {
const message = { ...baseIdentityImage } as IdentityImage;
if (object.payload !== undefined && object.payload !== null) {
message.payload = object.payload;
} else {
message.payload = new Uint8Array();
}
if (object.sourceType !== undefined && object.sourceType !== null) {
message.sourceType = object.sourceType;
} else {
message.sourceType = 0;
}
if (object.imageType !== undefined && object.imageType !== null) {
message.imageType = object.imageType;
} else {
message.imageType = 0;
}
return message;
},
};
declare var self: any | undefined;
declare var window: any | undefined;
declare var global: any | undefined;
var globalThis: any = (() => {
if (typeof globalThis !== "undefined") return globalThis;
if (typeof self !== "undefined") return self;
if (typeof window !== "undefined") return window;
if (typeof global !== "undefined") return global;
throw "Unable to locate global object";
})();
const atob: (b64: string) => string =
globalThis.atob ||
((b64) => globalThis.Buffer.from(b64, "base64").toString("binary"));
function bytesFromBase64(b64: string): Uint8Array {
const bin = atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}
const btoa: (bin: string) => string =
globalThis.btoa ||
((bin) => globalThis.Buffer.from(bin, "binary").toString("base64"));
function base64FromBytes(arr: Uint8Array): string {
const bin: string[] = [];
for (const byte of arr) {
bin.push(String.fromCharCode(byte));
}
return btoa(bin.join(""));
}
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
function longToNumber(long: Long): number {
if (long.gt(Number.MAX_SAFE_INTEGER)) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
return long.toNumber();
}
if (_m0.util.Long !== Long) {
_m0.util.Long = Long as any;
_m0.configure();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import { utils } from "js-waku";
const hexToBuf = utils.hexToBuf;
export { hexToBuf };
/**
* Return hex string with 0x prefix (commonly used for string format of a community id/public key.
*/
export function bufToHex(buf: Uint8Array): string {
return "0x" + utils.bufToHex(buf);
}

View File

@ -1,12 +1,13 @@
import { keccak256 } from "js-sha3"; import { keccak256 } from "js-sha3";
import { utils } from "js-waku";
import { Reader } from "protobufjs"; import { Reader } from "protobufjs";
import secp256k1 from "secp256k1"; import secp256k1 from "secp256k1";
import { Identity } from "../identity";
import * as proto from "../proto/status/v1/application_metadata_message";
import { ApplicationMetadataMessage_Type } from "../proto/status/v1/application_metadata_message";
import { hexToBuf } from "../utils";
import { ChatMessage } from "./chat_message"; import { ChatMessage } from "./chat_message";
import { Identity } from "./identity";
import * as proto from "./proto/status/v1/application_metadata_message";
import { ApplicationMetadataMessage_Type } from "./proto/status/v1/application_metadata_message";
export class ApplicationMetadataMessage { export class ApplicationMetadataMessage {
private constructor(public proto: proto.ApplicationMetadataMessage) {} private constructor(public proto: proto.ApplicationMetadataMessage) {}
@ -69,6 +70,6 @@ export class ApplicationMetadataMessage {
const recid = this.signature.slice(64)[0]; const recid = this.signature.slice(64)[0];
const hash = keccak256(this.payload); const hash = keccak256(this.payload);
return secp256k1.ecdsaRecover(signature, recid, utils.hexToBuf(hash)); return secp256k1.ecdsaRecover(signature, recid, hexToBuf(hash));
} }
} }

View File

@ -0,0 +1,17 @@
import { Reader } from "protobufjs";
import * as proto from "../proto/communities/v1/chat_identity";
export class ChatIdentity {
public constructor(public proto: proto.ChatIdentity) {}
static decode(bytes: Uint8Array): ChatIdentity {
const protoBuf = proto.ChatIdentity.decode(Reader.create(bytes));
return new ChatIdentity(protoBuf);
}
encode(): Uint8Array {
return proto.ChatIdentity.encode(this.proto).finish();
}
}

View File

@ -1,5 +1,11 @@
import { expect } from "chai"; import { expect } from "chai";
import {
AudioMessage_AudioType,
ChatMessage_ContentType,
} from "../proto/communities/v1/chat_message";
import { ImageType } from "../proto/communities/v1/enums";
import { import {
AudioContent, AudioContent,
ChatMessage, ChatMessage,
@ -7,11 +13,6 @@ import {
ImageContent, ImageContent,
StickerContent, StickerContent,
} from "./chat_message"; } from "./chat_message";
import {
AudioMessage_AudioType,
ChatMessage_ContentType,
} from "./proto/communities/v1/chat_message";
import { ImageType } from "./proto/communities/v1/enums";
describe("Chat Message", () => { describe("Chat Message", () => {
it("Encode & decode Image message", () => { it("Encode & decode Image message", () => {

View File

@ -1,14 +1,14 @@
import { Reader } from "protobufjs"; import { Reader } from "protobufjs";
import * as proto from "./proto/communities/v1/chat_message"; import * as proto from "../proto/communities/v1/chat_message";
import { import {
AudioMessage, AudioMessage,
AudioMessage_AudioType, AudioMessage_AudioType,
ChatMessage_ContentType, ChatMessage_ContentType,
ImageMessage, ImageMessage,
StickerMessage, StickerMessage,
} from "./proto/communities/v1/chat_message"; } from "../proto/communities/v1/chat_message";
import { ImageType, MessageType } from "./proto/communities/v1/enums"; import { ImageType, MessageType } from "../proto/communities/v1/enums";
export type Content = export type Content =
| TextContent | TextContent

View File

@ -0,0 +1,56 @@
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";
export class CommunityChat {
public constructor(public proto: proto.CommunityChat) {}
/**
* Decode the payload as CommunityChat message.
*
* @throws
*/
static decode(bytes: Uint8Array): CommunityChat {
const protoBuf = proto.CommunityChat.decode(Reader.create(bytes));
return new CommunityChat(protoBuf);
}
encode(): Uint8Array {
return proto.CommunityChat.encode(this.proto).finish();
}
// TODO: check and document what is the key of the returned Map;
public get members(): Map<string, CommunityMember> {
const map = new Map();
for (const key of Object.keys(this.proto.members)) {
map.set(key, this.proto.members[key]);
}
return map;
}
public get permissions(): CommunityPermissions | undefined {
return this.proto.permissions;
}
public get identity(): ChatIdentity | undefined {
return this.proto.identity;
}
// TODO: Document this
public get categoryId(): string | undefined {
return this.proto.categoryId;
}
// TODO: Document this
public get position(): number | undefined {
return this.proto.position;
}
}

View File

@ -0,0 +1,93 @@
import debug from "debug";
import { WakuMessage, WakuStore } from "js-waku";
import { Reader } from "protobufjs";
import { idToContentTopic } from "../contentTopic";
import * as proto from "../proto/communities/v1/communities";
import { bufToHex } from "../utils";
import { ChatIdentity } from "./chat_identity";
import { CommunityChat } from "./community_chat";
const dbg = debug("communities:wire:community_description");
export class CommunityDescription {
private constructor(public proto: proto.CommunityDescription) {}
static decode(bytes: Uint8Array): CommunityDescription {
const protoBuf = proto.CommunityDescription.decode(Reader.create(bytes));
return new CommunityDescription(protoBuf);
}
encode(): Uint8Array {
return proto.CommunityDescription.encode(this.proto).finish();
}
/**
* Retrieves the most recent Community Description it can find on the network.
*/
public static async retrieve(
communityPublicKey: Uint8Array,
wakuStore: WakuStore
): Promise<CommunityDescription | undefined> {
const hexCommunityPublicKey = bufToHex(communityPublicKey);
const contentTopic = idToContentTopic(hexCommunityPublicKey);
let communityDescription: CommunityDescription | undefined;
const callback = (messages: WakuMessage[]): void => {
// Value found, stop processing
if (communityDescription) return;
// Process most recent message first
const orderedMessages = messages.reverse();
orderedMessages.forEach((message: WakuMessage) => {
if (!message.payload) return;
try {
const _communityDescription = CommunityDescription.decode(
message.payload
);
if (!_communityDescription.identity) return;
communityDescription = _communityDescription;
} catch (e) {
dbg(
`Failed to decode message as CommunityDescription found on content topic ${contentTopic}`,
e
);
}
});
};
await wakuStore
.queryHistory([contentTopic], {
callback,
})
.catch((e) => {
dbg(
`Failed to retrieve community description for ${hexCommunityPublicKey}`,
e
);
});
return communityDescription;
}
get identity(): ChatIdentity | undefined {
if (!this.proto.identity) return;
return new ChatIdentity(this.proto.identity);
}
get chats(): Map<string, CommunityChat> {
const map = new Map();
for (const key of Object.keys(this.proto.chats)) {
map.set(key, this.proto.chats[key]);
}
return map;
}
}